@twinsearth/w2m-dsh-plugin 2.0.8 → 2.0.10

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.
@@ -0,0 +1,162 @@
1
+ # 文件传输与分块层(Transfer Layer)
2
+
3
+ 状态:实现已存在(v2.0.8),本文固化契约;跨机链路仍受 P2P 握手/传输可靠性阻塞。
4
+ 代码:`src/agent/transfer/{manifest,object-store,reference,ferry,want-block,publish}.mjs`
5
+ 测试:`test/xfer-{foundation,ferry,publish,want-block}.test.mjs`
6
+
7
+ ## 1. 分层
8
+
9
+ ```
10
+ Content Addressing manifest + rootCid + ArtifactRef
11
+ Chunk Store 分块文件、bitmap、原子 finalize
12
+ Transfer Session want/chunk 窗口、逐块校验、续传
13
+ Delivery / Sink dir / git / ssh,artifact_ready
14
+ Transport P2P(UDP/iroh 可替换),relay 永不承载字节
15
+ ```
16
+
17
+ ## 2. 内容寻址
18
+
19
+ - 每块:`chunkHash[i] = SHA-256(chunkBytes[i])`;
20
+ - 根:对“有序块摘要 + 块数 + 总大小 + 版本标签”做域分离哈希,得到 `rootCid = "sha256:<hex>"`;
21
+ - `ArtifactRef v2` 至少包含:
22
+ ```json
23
+ { "rootCid", "size", "chunkCount", "chunkBytes",
24
+ "originPeer", "sinkHint", "createdAt", "version": 2 }
25
+ ```
26
+ - 读取端只接受 `verifyRefV2(ref)` 通过的引用;根不一致即拒绝,不做“尽力修复”。
27
+
28
+ ## 3. 原语契约
29
+
30
+ ### chunkFile(path, { chunkBytes })
31
+
32
+ - 输出 `{ ok, rootCid, size, chunkCount, chunkBytes, chunks[] }`;
33
+ - `chunkBytes` 默认 256 KiB,最小 16 KiB,最大 2 MiB;
34
+ - 分块读取,不一次性载入整个文件;
35
+ - 空文件/短读/权限错误返回具名错误码。
36
+
37
+ ### createReceiver({ stateDir, sinkRoot, ref, destination })
38
+
39
+ - 建立 `<stateDir>/<transferId>/data.part` 与 `bitmap.json`;
40
+ - `destination` 必须经 `safeRelative` 校验,拒绝 `..`、绝对路径、盘符、NUL;
41
+ - 恢复时读取 bitmap,只请求缺失块。
42
+
43
+ ### writeChunk(index, data, expectedSha256)
44
+
45
+ - 先校验 `index` 范围、`data.length`、`SHA-256(data) == expected`,全部通过才写入;
46
+ - 写 `data.part` 对应 offset,随后原子更新 bitmap(temp+rename);
47
+ - 重复块:哈希一致则幂等成功,不一致拒绝 `XFER_CHUNK_HASH_MISMATCH`;
48
+ - 不信任调用方给出的长度/哈希。
49
+
50
+ ### finalizeTransfer(rootCid)
51
+
52
+ - 要求 bitmap 全满;
53
+ - 重算根(不信任 bitmap),与 `ref.rootCid` 比较;
54
+ - `fsync` 数据与目录,`rename` 到最终路径(同目录,原子);
55
+ - 幂等:已 finalize 且哈希一致则返回成功,不重复覆盖。
56
+
57
+ ## 4. 可恢复传输
58
+
59
+ - bitmap 持久化 `<stateDir>/<transferId>/bitmap.json`,含 `chunks/chunk_bytes/last_chunk_bytes/hashes`;
60
+ - 崩溃恢复:只信任 bitmap 中标记完成且哈希复核通过的块;
61
+ - 会话状态:`transferId`、`rootCid`、已收块集合、最后错误;
62
+ - 传输中断不删除 `data.part`,由显式 abort/GC 清理;
63
+ - 同一 `rootCid` 允许多次会话,但同一 `transferId` 必须单写者(CAS/锁)。
64
+
65
+ ## 5. 传输协议(want-block)
66
+
67
+ ```
68
+ xfer.want { transferId, rootCid, indices[] } // 窗口 ≤ 8
69
+ xfer.chunk { transferId, index, sha256, size, data }
70
+ xfer.error { transferId, code, index? }
71
+ xfer.done { transferId }
72
+ ```
73
+
74
+ - 接收端按 bitmap 缺口发窗口请求;
75
+ - 每块独立校验,坏块只重试该块;
76
+ - 发送端不得读取/发送未请求的块;
77
+ - 流控:窗口上限 + 超时重发;irho/UDP 只替换传输,不改帧语义;
78
+ - relay 只承载 ref/元数据(publishRef),**永不承载 chunk 字节**。
79
+
80
+ ## 6. 安全与限制
81
+
82
+ | 项 | 规则 |
83
+ |---|---|
84
+ | 路径 | `safeRelative`:拒绝穿越、绝对路径、盘符、保留名 |
85
+ | 大小 | 单块 ≤2 MiB;单文件上限由配置控制;防止磁盘打满 |
86
+ | 哈希 | SHA-256;算法升级需新版本标签,不静默混用 |
87
+ | 权限 | sink 目录 0600/0700;不写入 stateDir 之外 |
88
+ | 元数据 | 只发布 ref;不发布路径、token、聊天明文 |
89
+ | 拒绝 | 无效 ref/哈希/索引 → 具名错误,不重试到无限 |
90
+
91
+ ## 7. 错误码(稳定面)
92
+
93
+ ```
94
+ XFER_SOURCE_INVALID / XFER_ROOT_INVALID / XFER_CHUNK_INDEX_INVALID
95
+ XFER_CHUNK_LENGTH_INVALID / XFER_CHUNK_HASH_MISMATCH
96
+ XFER_WINDOW_INVALID / XFER_TRANSFER_MISMATCH / XFER_ROOT_MISMATCH
97
+ XFER_READ_SHORT / XFER_FINALIZE_INCOMPLETE / XFER_PATH_UNSAFE
98
+ XFER_CHUNK_TOO_LARGE / XFER_TIMEOUT / P2P_NO_CANDIDATES / P2P_UNREACHABLE
99
+ ```
100
+
101
+ ## 8. 当前实现与缺口
102
+
103
+ 已实现并测试:
104
+
105
+ - chunkFile/分块校验/根计算(foundation 6/6);
106
+ - receiver/writeChunk/bitmap/续传/穿越拒绝(ferry 6/6);
107
+ - publishRef/listPublishedRefs(publish 3/3,真实 relay 验证);
108
+ - want-block 窗口/逐块校验/篡改拒绝(want-block 4/4);
109
+ - 同机 loopback 与 sender/receiver 候选探测(verified=true)。
110
+
111
+ 缺口(阻塞跨机):
112
+
113
+ 1. P2P 加密握手未完成(PH-03);
114
+ 2. UDP 分片 ACK/重传(P2P_FRAGMENT_UNACKED);
115
+ 3. 跨机 sender/receiver(XF-05)需 DSH/macmini 可执行;
116
+ 4. 断点续传跨机演练(XF-06);
117
+ 5. 大文件吞吐与背压(当前窗口 8 × 16 KiB 级)。
118
+
119
+ ## 9. 验收
120
+
121
+ - 根与逐块哈希全部一致;
122
+ - 任意中断后重启,最终文件 SHA-256 与源一致;
123
+ - 路径穿越/篡改/错索引全部具名拒绝;
124
+ - relay 侧抓包/日志无文件字节;
125
+ - 两机 E2E:publish → want → finalize → artifact_ready;
126
+ - 性能目标(待定):LAN 1 GiB < 60s,跨网按 P2P 传输能力单独定。
127
+
128
+ ## 10. iroh-blobs / BLAKE3 评估(参考,不立即替换)
129
+
130
+ iroh-blobs 提供:BLAKE3 内容寻址、Merkle 树 verified streaming、天然断点续传、多源 want-list。
131
+ W2M 现状:SHA-256 + 扁平块哈希 + bitmap + 单源 want-block。
132
+
133
+ | 能力 | W2M 现状 | iroh-blobs | 结论 |
134
+ |---|---|---|---|
135
+ | 哈希 | SHA-256(OpenSSL/JS 可用) | BLAKE3(更快) | 不急着换;BLAKE3 需纯 JS/原生依赖 |
136
+ | 流式校验 | 逐块到达即校验 | Merkle 证明,支持部分验证 | 可后续加 Merkle,不破坏现有 ref |
137
+ | 断点续传 | bitmap 已实现 | 协议内建 | 已等价,无需替换 |
138
+ | 多源 | 单源 | 块级多 peer 并行 | 值得自研 want-list 调度,不必引入完整栈 |
139
+ | 依赖 | 零依赖 | Rust/原生绑定 | 与 tarball/Electron 约束冲突 |
140
+
141
+ 建议:
142
+
143
+ 1. **v2.x 保持 SHA-256 + bitmap**,不改变 ArtifactRef v2 与现有测试;
144
+ 2. **v2.2.2 增加 `ArtifactRef v3`(feature flag)**:
145
+ ```json
146
+ { "rootCid", "hashAlg": "blake3"|"sha256", "merkleRoot",
147
+ "chunkBytes", "chunkCount", "size", "originPeer", "version": 3 }
148
+ ```
149
+ 双哈希过渡:v3 同时携带 `sha256Root`,旧端只读 v2,新端优先 v3;
150
+ 3. **多源调度先自研**:want-list + peer 评分(时延/丢包/已成功率)+ 块租约,避免重复下载;
151
+ 4. **iroh-blobs 只作为 sidecar 试验**:与 iroh 传输一起评估,不进入默认分发;
152
+ 5. 验收:同一文件 v2/v3 root 可互证;中断续传 SHA-256 一致;多源重复块不重复写;无 BLAKE3 运行时回退 SHA-256 并记录 `hashAlg`。
153
+
154
+ 风险:BLAKE3 在 Electron 可能同样缺原生实现(需纯 JS 回退);Merkle 证明增加协议面;多源引入 Sybil/污染块,必须逐块哈希校验并限制来源。
155
+
156
+
157
+ - 根与逐块哈希全部一致;
158
+ - 任意中断后重启,最终文件 SHA-256 与源一致;
159
+ - 路径穿越/篡改/错索引全部具名拒绝;
160
+ - relay 侧抓包/日志无文件字节;
161
+ - 两机 E2E:publish → want → finalize → artifact_ready;
162
+ - 性能目标(待定):LAN 1 GiB < 60s,跨网按 P2P 传输能力单独定。
@@ -0,0 +1,19 @@
1
+ {
2
+ "version": "2.0.10",
3
+ "case": "RELEASE-2100",
4
+ "kind": "positive",
5
+ "machines": [
6
+ "b53a37f4-1464-4512-9259-33a7df091e3e"
7
+ ],
8
+ "executed_at": "2026-10-30T21:19:15.294Z",
9
+ "executor": "lead",
10
+ "task_id": "pre-release-2100-1793394641623",
11
+ "requirement": "v2.0.10: ship remote-view S2 capture probe as a script (no node -e)",
12
+ "result": {
13
+ "outcome": "ok",
14
+ "assertions": {
15
+ "s2_script": "bin/remote-s2-capture.mjs shipped (scripts/ is forbidden in the release tarball); summaryBits/platform test 2/2",
16
+ "preflight": "GREEN: 16 pass, 0 fail, 1 host-only UNVERIFIABLE; 3299 tests / 244 files"
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "version": "2.0.9",
3
+ "case": "RELEASE-2090",
4
+ "kind": "positive",
5
+ "machines": [
6
+ "b53a37f4-1464-4512-9259-33a7df091e3e"
7
+ ],
8
+ "executed_at": "2026-10-30T04:33:38.890Z",
9
+ "executor": "lead",
10
+ "task_id": "pre-release-2090-1793334046781",
11
+ "requirement": "v2.0.9: P2P reliability (ACK/RTT/window), transport abstraction + race/scheduler, iroh sidecar contract, xfer router/ref v3, fleet diagnostics, status bar",
12
+ "result": {
13
+ "outcome": "ok",
14
+ "assertions": {
15
+ "p2p_reliability": "p2p-transport 28/28; RTT estimator 3/3; benchmark 4 MiB loopback 15.07 MiB/s",
16
+ "transport_abstract": "transport 3/3, race 4/4, scheduler 4/4, discovery 3/3",
17
+ "iroh_sidecar": "fake-sidecar contract 4/4 (real binary pending infra)",
18
+ "xfer": "channel 2/2, router 2/2, recovery 2/14 combined, ref-v3 4/4",
19
+ "release": "preflight to be recorded before tag",
20
+ "preflight": "GREEN: 16 pass, 0 fail, 1 host-only UNVERIFIABLE",
21
+ "build": "dist/twinsearth-w2m-dsh-plugin-2.0.9.tgz sha256 dee553dc10c3a26107803517cb5a0f6bc010b31c70fe2e4e7a74371fc975aac7"
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,67 @@
1
+ # ADR 0002: P2P 传输路线(iroh 优先 + 多方案并行)
2
+
3
+ 状态:Accepted(2026-10-30)
4
+ 关联:ADR 0001(CRDT)、TRANSFER-LAYER.md、TEST-PLAN-JOINT-FLEET-2.0.8.md
5
+
6
+ ## 决策
7
+
8
+ 1. **传输抽象先行**:定义 `P2PTransport`(start/stop/send/onMessage/paths/diagnostics),
9
+ 现有 UDP 实现为 `udp-v1`,iroh 为 `iroh-v1`,libp2p 仅作候选发现。
10
+ 2. **iroh 优先,但不阻塞 UDP**:双方都声明 `p2p.transports` 时并行起跑,首个完成
11
+ 加密握手 + 首帧 ACK 的路径胜出;总预算 ≤3s;失败具名,不静默回退。
12
+ 3. **iroh 以 sidecar 集成**:不把原生 addon 加载进 Electron 主进程;子进程 IPC,
13
+ 失败/超时可单独回滚;默认分发仍可只带 UDP。
14
+ 4. **自建 iroh-relay(TLS 域名 + 80/443 + ACME)**:作为可选基础设施;
15
+ 不复用 8787 relay,不修改 relay 2.0.5。
16
+ 5. **中继语义边界**:允许 DERP 式**密文转发**;relay 不得解密、不得落盘、
17
+ 不得记录内容;仅保留必要的连接元数据与速率限制。
18
+ 6. **平台范围**:Apple Silicon macOS 为唯一 macOS 目标;不支持 Intel Mac;
19
+ 不因 Intel 预编译缺失而阻塞 iroh 路线。
20
+ 7. **身份**:iroh `EndpointId`(Ed25519)与现有 `device.key`(X25519)独立派生:
21
+ `hkdf(seed, 'iroh-ed25519')`;通过 offer 的 `p2p.iroh_ids` 交换,relay 不改协议。
22
+ 8. **数据面与文件层解耦**:iroh/UDP 只替换传输;`xfer.want/chunk`、Merkle、
23
+ ArtifactRef 语义保持不变。
24
+
25
+ ## 立即优先级(在 iroh 之前)
26
+
27
+ - P0-a:chunk 帧接入 `P2PChannel`,不再裸 `dgram.send`;
28
+ - P0-b:`onAck` 触发 `pump()`,窗口随 ACK/BDP 推进(当前 0.12 MiB/s 的结构性上限);
29
+ - P0-c:`verifyRefV2` 自洽校验 + 统一两套 v2 ref;
30
+ - P1:SHA-256 Merkle 树(逐块证明,作为多源准入条件);
31
+ - 之后才是 iroh sidecar 时间盒。
32
+
33
+ ## 验收
34
+
35
+ | 指标 | 目标 |
36
+ |---|---|
37
+ | 加密覆盖率 | 同版本、双方有 key:100% |
38
+ | 跨机直连率 | 同 LAN ≥90%,跨网 ≥70% |
39
+ | 首帧时延 p95 | 直连 <300ms |
40
+ | 中继边界 | relay 无明文、无落盘、无内容日志 |
41
+ | 回滚 | `p2p.transports=['udp']` 即回退,无需重装 |
42
+ | 平台 | macOS arm64 + Windows x64 通过;Intel Mac 明确不支持 |
43
+
44
+ ## 方案定位(冻结)
45
+
46
+ | 方案 | 定位 | 优势 | 短板 | 是否进入数据面 |
47
+ |---|---|---|---|---|
48
+ | **iroh** | 首选、高打洞成功率 | QUIC 原生打洞、DERP 回落、路径选择、加密/重传内建 | 需 sidecar + 自建 TLS relay;JS 绑定/原生二进制 | ✅ 是 |
49
+ | **现有 UDP** | 默认兜底、零新增依赖 | 已有加密栈、wire 兼容、无新进程 | 可靠性仍缺 RTT 采样/拥塞控制/PMTU | ✅ 是 |
50
+ | **libp2p** | 可选“全家桶”参照 | 模块化、DHT 发现、多传输 | 打洞率约 70%、依赖重、复杂度高 | ❌ 否,仅发现层 |
51
+
52
+ 规则:
53
+
54
+ 1. 数据面只允许 `iroh` 与 `udp`;`libp2p` 不得接管 send/ACK/加密;
55
+ 2. libp2p 只作为 `discovery` provider(DHT/mDNS)产出候选,候选仍需 relay/offer 身份校验;
56
+ 3. 未声明 iroh 的对端只走 UDP,行为与 v2.0.8 一致;
57
+ 4. 竞速失败返回 `ALL_TRANSPORTS_FAILED`,数据面不回落 relay;
58
+ 5. 发现层依赖放 sidecar,服务端 `dependencies` 保持为空;
59
+ 6. DHT 记录必须签名,失败丢弃并记 `DHT_RECORD_INVALID`。
60
+
61
+ ## 风险
62
+
63
+ - iroh JS 绑定滞后(npm 1.2.0 vs 上游 1.4.1):sidecar 锁版本;
64
+ - 自建 relay 的域名/证书/CN→JP RTT:上线前实测;
65
+ - sidecar 崩溃/升级:独立进程 + 健康检查 + 自动回退 UDP;
66
+ - 多源投毒:Merkle 未验收前禁止开多源;
67
+ - 中继流量合规:只转发密文、限速、不记内容,写入运维文档。
@@ -0,0 +1,153 @@
1
+ # v2.1.0 远程桌面子插件(仅查看)落地方案
2
+
3
+ 状态:DRAFT,待 P0 spike 与决策确认;未改代码、未发布。
4
+ 专家评审:国内网络架构工程师 + 视频流工程师(只读)。
5
+ 目标:设置页开关;开启后侧边栏出现 iMac 图标标签;仅查看、不可控制;后续版本再开放鼠标操控。
6
+
7
+ ## 1. 结论
8
+
9
+ 1. v2.1.0 的现实目标是 **加密、可用的 1–2 fps 查看**,不是 RustDesk 级 30fps 远控;10fps+ 需要 sidecar/不可靠传输,排 P4。
10
+ 2. **不 vendor RustDesk/hbb_common**(AGPL 边界 + 与现有 P2P 重复);只借鉴分层与自适应思路,sidecar 必须可选、独立进程、外部二进制。
11
+ 3. 先做 P0 三个 spike(浏览器流、macOS TCC/采集、VK 握手),再写 UI;UI 是最不会失败的部分。
12
+ 4. 媒体 E2E 加密,relay 只做信令/授权/密文回退;目标端默认拒绝,每次会话显式同意。
13
+ 5. v2.1.0 不做:输入控制、剪贴板、文件、音频、录制、无人值守、多查看者、Windows 被查看端。
14
+
15
+ ## 2. 分层
16
+
17
+ ```
18
+ L5 浏览器:sidebar iMac 标签 + canvas 查看页 + 设置页开关
19
+ L4 DSH 宿主:remote-view route/service/session(同源 /api/w2m/remote/*)
20
+ L3 agent:view 会话、采集、同意、帧加密(view.* P2P 路由)
21
+ L2 传输:P2PChannel(优先)| relay 密文帧(回退);VK 会话加密
22
+ L1 relay:/v1/remote/signal、/v1/remote/stream、/v1/remote/frame(只密文)
23
+ L0 平台:macOS screencapture/sips(P0)→ ScreenCaptureKit(P3/P4)
24
+ ```
25
+
26
+ 同包内子模块 `src/plugin/remote-view/*` + `src/agent/view/*`,不拆 npm 包;单版本、单 patch row。
27
+
28
+ ## 3. UI 接口
29
+
30
+ - `settings.section`:远程桌面开关(默认关闭),写 `<stateDir>/remote-view.json`(0600)。
31
+ - `sidebar.panellist`:iMac 图标,仅开关开启时渲染;关闭时返回 null。
32
+ - `main`:查看页(目标选择、同意状态、canvas、直连/中继徽标、停止、“仅查看”常驻说明)。
33
+ - 每个 slot 独立 try/catch,不新增 inject;动态显隐需 P0 验证外壳是否折叠空行,失败则提示刷新。
34
+
35
+ ## 4. 信令与安全
36
+
37
+ - 身份:fleet 准入(relay)+ X25519 device key(通道认证)+ 临时 X25519 三重 DH 派生 VK(媒体 E2E)。
38
+ - invite/accept:`k_mac = HKDF(ECDH(static keys))` 密封;VK 派生后媒体/控制分方向密钥。
39
+ - 目标端授权:默认拒绝;每次 `osascript display dialog`,30s 超时;可选内存态“允许 30 分钟”,不落盘。
40
+ - relay:同 fleet 校验;只接受 `{session_id, seq, ct}`;拒绝 JPEG/PNG magic、明文 JSON、输入类型;TLS 强制(否则 `VIEW_RELAY_INSECURE`)。
41
+ - 错误码:VIEW_DISABLED/TARGET_BUSY/NO_ANSWER/CONSENT_DENIED/P2P_TIMEOUT/CAPTURE_DENIED/PLAINTEXT_MEDIA_REFUSED 等。
42
+
43
+ ## 5. 视频链路(专家修订版)
44
+
45
+ ### 5.1 安全上下文:不是部署要求
46
+
47
+ 页面从本机 DSH GUI(`127.0.0.1`)加载,属于 potentially trustworthy origin;媒体帧由目标机经 P2P/relay 到本机宿主,再由同源 route 给浏览器。因此:
48
+
49
+ | 拓扑 | 页面 origin | isSecureContext | WebCodecs | P1 |
50
+ |---|---|---|---|---|
51
+ | A 本机 DSH 面板 | 127.0.0.1 | true | 可用 | 主路径 |
52
+ | B 本机 DSH + HTTPS | https | true | 可用 | 可选 |
53
+ | C 局域网直开 DSH | http://192.168.x.x | false | 不可用 | 仅 JPEG 档 |
54
+ | D 局域网 HTTPS 反代 | https | true | 可用 | 需证书运维 |
55
+
56
+ P1 不要求 HTTPS;运行时探针 `isSecureContext / VideoDecoder / createImageBitmap`,C 拓扑只走 JPEG。不要把整个 DSH GUI 绑到 0.0.0.0;局域网只读查看应使用独立端口 + 一次性 token。
57
+
58
+ ### 5.2 渲染:SSE + createImageBitmap(P1 默认)
59
+
60
+ 每帧独立 JPEG/WebP/AVIF,经同源 SSE 下发:
61
+ `{ session_id, seq, ts_ms, w, h, mime, bytes, data_b64 }`;无变化发 `view.unchanged`。
62
+ 前端 `createImageBitmap(new Blob([bytes]))` → canvas `drawImage`;不需要 WebCodecs,不需要 HTTPS。
63
+ “最新帧优先”:上一帧未投递完就丢新帧(dropped++),不排队。
64
+
65
+ ### 5.3 编解码:帧内优先,视频档可选
66
+
67
+ | 层 | 编码 | 1–2fps 收益 | 结论 |
68
+ |---|---|---|---|
69
+ | 帧内 | JPEG | 地板,永可用 | P1 默认 |
70
+ | 帧内 | WebP | 体积 0.65–0.75× | P1.5 升级 |
71
+ | 帧内 | AVIF/HEIC | 0.5–0.7×,编码 100–300ms | P2(需 ffmpeg/Safari) |
72
+ | 帧间 | H.264/HEVC/AV1 | 1–2fps 下 IDR/GOP 成本抵消收益 | 可选加速档,非地板 |
73
+ | 帧间 | VP9 | 无 Apple 硬编 | 不建议 |
74
+
75
+ “不接受仅 H.264”的落法:**P1 的媒体地板不是 H.264,而是 JPEG**;HEVC/AV1 作为探测后的可选加速档,缺失不影响可用性。
76
+ 1–2fps 最有效的省流不是换编码器,而是“画面未变”抑制(可砍 50–80% 帧)。
77
+
78
+ ### 5.4 macOS 采集
79
+
80
+ - P1:`screencapture -x -t jpg -D 1`(1fps 稳,2fps 边际);relay 档再加 `sips -Z 1024 -s formatOptions 45`。
81
+ - 2fps 可选:`ffmpeg -f avfoundation -framerate 2 -c:v mjpeg` 常驻,外部二进制、不打包、缺失即回落。
82
+ - TCC:Screen Recording 按责任进程归属;必须区分 `CAPTURE_DENIED / CAPTURE_BLANK / CAPTURE_FAILED`,目标端 GUI 给系统设置跳转。
83
+ - 采集 argv 固定,不走 allow-list。
84
+
85
+ ### 5.5 同意 UI(目标端 DSH GUI)
86
+
87
+ 目标端也跑 DSH GUI:同意弹窗显示 viewer/屏幕/分辨率/fps/时长;允许后常驻“正在被查看”横幅 + 停止按钮;默认拒绝、30s 超时、内存态授权 ≤30min;锁屏/关闭面板/停止 → 停采 ≤1s。headless 才用 osascript 兜底。
88
+
89
+ ### 5.6 帧预算与传输
90
+
91
+ | 通道 | 单帧 raw 上限 | 目标 | 默认 fps | 超限行为 |
92
+ |---|---|---|---|---|
93
+ | P2P | 96 KiB | 64 KiB | 1(P1 只承诺 1fps) | 降分辨率,不分块 |
94
+ | relay | 47 KiB 硬顶 | 40 KiB | 1 | 降分辨率/1fps |
95
+
96
+ - relay 只接受 `{session_id, seq, ct}`;拒绝 JPEG/PNG magic、明文 JSON、输入类型;媒体必须 E2E 加密(TLS 不能替代)。
97
+ - 不把帧塞进 `/v1/fleet/messages`(4 KiB + 配额 + 通知不带正文)。
98
+ - 新 feature:`remote-frame-v1`;老 relay → `VIEW_RELAY_UNSUPPORTED`,不发第一帧。
99
+ - P1 建议:P2P 媒体 + relay 信令;relay 密文帧放 P1.5(40 KiB 清晰度有限,先做逃生门)。
100
+
101
+ ### 5.7 P0 spike(3–4 人日)
102
+
103
+ 1. S1 JPEG→canvas 1–2fps 渲染(≤30ms/帧);
104
+ 2. S2 macOS 采集 1/2fps + TCC 三形态(先跑,唯一能推翻 fps 目标);
105
+ 3. S3 安全上下文矩阵(127.0.0.1 vs 局域网 IP);
106
+ 4. S4 P2P 40/64/96/128 KiB 单帧预算;
107
+ 5. S5 relay 47 KiB 边界/413/magic 拒绝;
108
+ 6. S6 同意/撤销六态。
109
+
110
+ ## 6. 阶段
111
+
112
+ | 阶段 | 目标 | 退出条件 | 人日 |
113
+ |---|---|---|---|
114
+ | P0 | 证伪浏览器流/TCC/VK 三假设 | 首帧延迟、采集耗时、权限拒绝行为、VK 单测 | 3–5 |
115
+ | P1 | 最小可用:P2P JPEG + relay 信令 + UI 开关/标签/同意 | Windows 看 Mac 1fps;错误码齐全;relay 无媒体明文 | 8–12 |
116
+ | P2 | 隐私/生命周期硬化 | 30min soak、锁屏暂停、限流、默认拒绝 | 5–8 |
117
+ | P3 | 密文中继回退 + TLS + 384KiB 帧上限 | 阻断 UDP 后仍 1fps;relay 无 JPEG magic | 8–12 |
118
+ | P4 | 可选 sidecar(非 RustDesk)10–15fps + 窗口选择 | sidecar 缺失时 P1 仍可用 | 10–15 |
119
+
120
+ ## 7. 验收(节选)
121
+
122
+ - 开关关闭:无标签、`start` 返回 VIEW_DISABLED、无 UDP bind/relay stream。
123
+ - 开关开启:iMac 标签出现;roster 只列 `caps.view` 的 macOS 机器。
124
+ - 同意:拒绝/超时/忙/离线均有具名 code;默认拒绝。
125
+ - 性能:LAN 首帧 ≤2.5s;1fps 持续 10 分钟;停止 ≤1s;中继 ≥0.8fps。
126
+ - 安全:relay 请求体无 `FFD8FF`/`image/jpeg`;发真 JPEG 到 frame 端点返回 400;输入类型返回 VIEW_INPUT_FORBIDDEN。
127
+ - 零依赖:`dependencies` 不变;client bundle 测试通过;旧 relay/旧 agent 有明确降级。
128
+
129
+ ## 8. 决策与待确认
130
+
131
+ 已确认:
132
+
133
+ 1. 接受 1–2 fps 查看;
134
+ 2. relay 有 TLS;
135
+ 3. macOS 目标端也跑 DSH GUI(同意 UI 放目标端 GUI);
136
+ 4. 不接受仅 H.264 → 媒体地板改为 JPEG,HEVC/AV1 作为可选加速档;
137
+ 5. 允许局域网 HTTP → C 拓扑只走 JPEG,不要求 WebCodecs;
138
+ 6. **relay 密文帧路径进 P1.5**;P1 为 P2P 媒体 + relay 信令,P2P 失败明确 `P2P_UNAVAILABLE`;
139
+ 7. **P1 不包含 2fps 档**:只有 1fps 的对外承诺;2fps 在 S2 实测达标后仅作为隐藏实验开关,不进 UI/验收/文档承诺。
140
+ 8. **允许 2fps 依赖外部 ffmpeg**(面向 P1.5):可选、不打包、不自动下载、缺失回落;`dependencies` 保持为空。
141
+
142
+ P1 基线(无 ffmpeg):`screencapture` 1fps + JPEG;有 ffmpeg:2fps 档可选。
143
+
144
+ ## 9. 2fps 决策(2026-10-30)
145
+
146
+ 结论:P1 不设 2fps 交付档位。
147
+
148
+ - 唯一保证的采集后端 screencapture 单帧 p50 215–360ms / p95 590ms,2fps 的 500ms 预算越界;
149
+ - 2fps 的收益仅约 250–450ms 新鲜度,1fps 与 2fps 同属幻灯片(8–15fps 才是感知门槛);
150
+ - 1% 丢包下 57 分片帧受损概率 43%,一次 RTO 300ms 吃掉 2fps 预算 60%;
151
+ - P1 的坑应在同意/TCC/撤销,不在帧率;2fps 留 P1.5 视频档或 S2 后隐藏开关。
152
+
153
+ 替代方案(P1 推荐):1fps + 变化追帧 burst(检测到变化时最多 2.5fps 持续 0.75s,令牌桶)+ 首帧双发 + 帧龄 HUD + unchanged 抑制读数。
package/lib/client.js CHANGED
@@ -501,6 +501,11 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
501
501
  '.w2mChat_emoji{padding:2px 4px;border:0;background:transparent;font-size:16px;line-height:1;cursor:pointer}',
502
502
  '.w2mFleetStatus{display:flex;gap:16px;align-items:center;justify-content:center;width:100%;padding:2px 12px;font-size:11px;opacity:.8;white-space:nowrap}',
503
503
  '.w2mFleetStatusItem{white-space:nowrap}',
504
+ '.w2mRelayItem{display:inline-flex;align-items:center;gap:4px}',
505
+ '.w2mRelayDot{width:8px;height:8px;border-radius:50%;display:inline-block;background:var(--dsw-alias-state-idle-primary)}',
506
+ '.w2mRelayDot[data-state="green"]{background:var(--dsw-alias-state-success-primary)}',
507
+ '.w2mRelayDot[data-state="yellow"]{background:var(--dsw-alias-state-warn-primary)}',
508
+ '.w2mRelayDot[data-state="red"]{background:var(--dsw-alias-state-danger-primary)}',
504
509
  '.w2mSearchToggle{height:26px;width:26px;border:0;border-radius:6px;background:transparent;color:inherit;font-size:14px;cursor:pointer}',
505
510
  '.w2mSearchToggle:hover{background:rgba(127,127,127,.14)}',
506
511
  '.w2mSearchBox{display:flex;align-items:center;gap:4px;padding:2px 4px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-background-secondary)}',
@@ -2080,12 +2085,33 @@ function TargetSelect({ sessionId, locked: _locked, t }) {
2080
2085
  return () => { alive = false; clearInterval(timer); };
2081
2086
  }, []);
2082
2087
  const fleet = snapshot?.fleet ?? null;
2083
- const server = snapshot?.server ?? null;
2088
+ const relays = snapshot?.relays ?? null;
2089
+ const server = relays?.server ?? null;
2084
2090
  const fleetText = fleet && Number.isFinite(fleet.online) && Number.isFinite(fleet.total) ? fleet.online + '/' + fleet.total : '—';
2085
2091
  const serverText = server && Number.isFinite(server.devices_online) && Number.isFinite(server.devices_total) ? server.devices_online + '/' + server.devices_total : '—';
2086
- return h('div', { className: 'w2mFleetStatus', title: '编队与服务器在线数量' }, [
2092
+ const relayItems = Array.isArray(relays?.shown) ? relays.shown.slice(0, 3) : [];
2093
+ const relayLabel = (index) => (['主', '备1', '备2'][index] ?? ('备' + index));
2094
+ const relayDot = (relay) => {
2095
+ if (relay?.ok !== true) return 'red';
2096
+ if (Number.isFinite(relay.rtt_ms) && relay.rtt_ms > 500) return 'yellow';
2097
+ return 'green';
2098
+ };
2099
+ const relayTitle = (relay) => {
2100
+ if (relay?.ok !== true) return (relay?.url ?? 'relay') + ':离线' + (relay?.error ? '(' + relay.error + ')' : '');
2101
+ return (relay?.url ?? 'relay') + ':在线,' + (Number.isFinite(relay?.rtt_ms) ? relay.rtt_ms + 'ms' : '延迟未知');
2102
+ };
2103
+ return h('div', { className: 'w2mFleetStatus', title: '编队、全服与 Relay 在线状态' }, [
2087
2104
  h('span', { key: 'f', className: 'w2mFleetStatusItem' }, '本编队在线 ' + fleetText),
2088
- h('span', { key: 's', className: 'w2mFleetStatusItem' }, '全部机器在线 ' + serverText),
2105
+ h('span', { key: 's', className: 'w2mFleetStatusItem' }, '全服机器在线 ' + serverText),
2106
+ h('span', { key: 'r', className: 'w2mFleetStatusItem' }, [
2107
+ h('span', { key: 'label' }, '在线Relay '),
2108
+ ...(relayItems.length === 0
2109
+ ? [h('span', { key: 'none' }, '—')]
2110
+ : relayItems.map((relay, index) => h('span', { key: 'relay-' + index, className: 'w2mRelayItem', title: relayTitle(relay) }, [
2111
+ h('span', { key: 'dot', className: 'w2mRelayDot', 'data-state': relayDot(relay) }),
2112
+ h('span', { key: 'name' }, relayLabel(index)),
2113
+ ]))),
2114
+ ]),
2089
2115
  ]);
2090
2116
  }
2091
2117
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinsearth/w2m-dsh-plugin",
3
- "version": "2.0.8",
3
+ "version": "2.0.10",
4
4
  "description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 11 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -48,7 +48,9 @@
48
48
  "SECURITY.md",
49
49
  "CONTRIBUTING.md",
50
50
  ".claude-plugin",
51
- "codex"
51
+ "codex",
52
+ "TEST-PLAN-JOINT-FLEET-2.0.8.md",
53
+ "TEST-CASES-JOINT-FLEET-2.0.8.md"
52
54
  ],
53
55
  "engines": {
54
56
  "node": ">=20.19.0"
@@ -64,7 +66,7 @@
64
66
  "pack": "node scripts/pack.mjs",
65
67
  "reachability": "node scripts/reachability-check.mjs",
66
68
  "reachability:classify": "node scripts/reachability-classify.mjs",
67
- "test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs test/update-channels.test.mjs test/update-prerelease.test.mjs test/session-watch.test.mjs test/session-readonly.test.mjs test/session-sync.test.mjs test/fleet-chat-identity.test.mjs test/identity-reader.test.mjs test/intents.test.mjs test/dataflow.test.mjs test/council-fleet.test.mjs test/fleet-memory.test.mjs test/memory-view.test.mjs test/schema-spec.test.mjs test/fleet-weight-parity.test.mjs test/task-deps.test.mjs test/dataflow-wiring.test.mjs test/task-graph.test.mjs test/ask-store.test.mjs test/turn.test.mjs test/fleet-members.test.mjs test/redact.test.mjs test/policy-matrix.test.mjs test/envelope-signature.test.mjs test/chat-crypto.test.mjs test/reduce-suggestion.test.mjs test/merge-hint.test.mjs test/fleet-chat-route.test.mjs test/maintenance.test.mjs test/maintenance-ferry.test.mjs test/grant.test.mjs test/grant-gate.test.mjs test/worktree.test.mjs test/patch-artifact.test.mjs test/merge-queue.test.mjs test/m3-workflow.test.mjs test/project-room.test.mjs test/project-attribution.test.mjs test/project-locks.test.mjs test/project-reviews.test.mjs test/project-notifications.test.mjs test/blob-sessions.test.mjs test/outbox.test.mjs test/reliability-metrics.test.mjs test/evidence.test.mjs test/grant-issue.test.mjs test/project-refs.test.mjs test/exec-capability.test.mjs test/secondary-link.test.mjs test/bootstrap-cli.test.mjs test/bootstrap.test.mjs test/windows-hidden-task.test.mjs test/relay-failover.test.mjs test/npm-update.test.mjs test/agent-start-nonblocking.test.mjs test/fleet-chat-capture.test.mjs test/capture-route.test.mjs test/update-resolver.test.mjs test/agent-identity-safe.test.mjs test/re-pair-stale-token.test.mjs test/gpw-folder-policy.test.mjs test/gpw-activation.test.mjs test/gpw-hardening.test.mjs test/gpw-tamper-gate.test.mjs test/gpw-persistence.test.mjs test/gpw-colors.test.mjs test/gpw-recent-fold.test.mjs test/gpw-auto-approve.test.mjs test/joint-dev-dispatch.test.mjs test/joint-dev-loop.test.mjs test/joint-dev-audit.test.mjs test/joint-dev-redline.test.mjs test/relay-redline-guard.test.mjs test/relay-p0-projection.test.mjs test/guarded-dispatch.test.mjs test/p0-single-matcher.test.mjs test/path-policy.test.mjs test/path-policy-fs.test.mjs test/gpw-inheritance-table.test.mjs test/gpw-policy-adapter.test.mjs test/target-policy.test.mjs test/relay-redline-audit.test.mjs test/relay-repair-ledger.test.mjs test/relay-purge-blobs.test.mjs test/relay-scrub.test.mjs test/joint-dev-plan-store.test.mjs test/relay-basepath-blobs.test.mjs test/operator-token.test.mjs test/relay-health-capabilities.test.mjs test/joint-relay-dispatch.test.mjs test/operator-token-route.test.mjs test/relay-scrub-task-projection.test.mjs test/operator-key-approval.test.mjs test/relay-offer-journal.test.mjs test/joint-r5-wiring.test.mjs test/p1-hardening.test.mjs test/joint-merge-engine.test.mjs test/artifact-ref-dir-sink.test.mjs test/chat-sticker-guard.test.mjs test/relay-task-progress.test.mjs test/gpw-modes.test.mjs test/joint-cli.test.mjs test/joint-command-hash.test.mjs test/joint-dispatch-audit.test.mjs test/tools-cancel-auth.test.mjs test/fleet-mirror.test.mjs test/sink-git-ssh.test.mjs test/agent-sse-watchdog.test.mjs test/version-gate.test.mjs test/joint-merge-cli.test.mjs test/joint-progress.test.mjs test/project-room-truncation.test.mjs test/chat-crypto-aad.test.mjs test/agent-artifact-delivery.test.mjs test/dataplane-sink-route.test.mjs test/artifact-sink-e2e.test.mjs test/project-room-persistence.test.mjs test/project-room-disk-persistence.test.mjs test/project-stream-route.test.mjs test/chat-v2-gate.test.mjs test/xiangqi.test.mjs test/xfer-foundation.test.mjs test/xfer-ferry.test.mjs test/xfer-publish.test.mjs test/xfer-want-block.test.mjs test/p2p-cipher-diag.test.mjs test/chacha-fallback.test.mjs test/fleet-binding.test.mjs",
69
+ "test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs test/update-channels.test.mjs test/update-prerelease.test.mjs test/session-watch.test.mjs test/session-readonly.test.mjs test/session-sync.test.mjs test/fleet-chat-identity.test.mjs test/identity-reader.test.mjs test/intents.test.mjs test/dataflow.test.mjs test/council-fleet.test.mjs test/fleet-memory.test.mjs test/memory-view.test.mjs test/schema-spec.test.mjs test/fleet-weight-parity.test.mjs test/task-deps.test.mjs test/dataflow-wiring.test.mjs test/task-graph.test.mjs test/ask-store.test.mjs test/turn.test.mjs test/fleet-members.test.mjs test/redact.test.mjs test/policy-matrix.test.mjs test/envelope-signature.test.mjs test/chat-crypto.test.mjs test/reduce-suggestion.test.mjs test/merge-hint.test.mjs test/fleet-chat-route.test.mjs test/maintenance.test.mjs test/maintenance-ferry.test.mjs test/grant.test.mjs test/grant-gate.test.mjs test/worktree.test.mjs test/patch-artifact.test.mjs test/merge-queue.test.mjs test/m3-workflow.test.mjs test/project-room.test.mjs test/project-attribution.test.mjs test/project-locks.test.mjs test/project-reviews.test.mjs test/project-notifications.test.mjs test/blob-sessions.test.mjs test/outbox.test.mjs test/reliability-metrics.test.mjs test/evidence.test.mjs test/grant-issue.test.mjs test/project-refs.test.mjs test/exec-capability.test.mjs test/secondary-link.test.mjs test/bootstrap-cli.test.mjs test/bootstrap.test.mjs test/windows-hidden-task.test.mjs test/relay-failover.test.mjs test/npm-update.test.mjs test/agent-start-nonblocking.test.mjs test/fleet-chat-capture.test.mjs test/capture-route.test.mjs test/update-resolver.test.mjs test/agent-identity-safe.test.mjs test/re-pair-stale-token.test.mjs test/gpw-folder-policy.test.mjs test/gpw-activation.test.mjs test/gpw-hardening.test.mjs test/gpw-tamper-gate.test.mjs test/gpw-persistence.test.mjs test/gpw-colors.test.mjs test/gpw-recent-fold.test.mjs test/gpw-auto-approve.test.mjs test/joint-dev-dispatch.test.mjs test/joint-dev-loop.test.mjs test/joint-dev-audit.test.mjs test/joint-dev-redline.test.mjs test/relay-redline-guard.test.mjs test/relay-p0-projection.test.mjs test/guarded-dispatch.test.mjs test/p0-single-matcher.test.mjs test/path-policy.test.mjs test/path-policy-fs.test.mjs test/gpw-inheritance-table.test.mjs test/gpw-policy-adapter.test.mjs test/target-policy.test.mjs test/relay-redline-audit.test.mjs test/relay-repair-ledger.test.mjs test/relay-purge-blobs.test.mjs test/relay-scrub.test.mjs test/joint-dev-plan-store.test.mjs test/relay-basepath-blobs.test.mjs test/operator-token.test.mjs test/relay-health-capabilities.test.mjs test/joint-relay-dispatch.test.mjs test/operator-token-route.test.mjs test/relay-scrub-task-projection.test.mjs test/operator-key-approval.test.mjs test/relay-offer-journal.test.mjs test/joint-r5-wiring.test.mjs test/p1-hardening.test.mjs test/joint-merge-engine.test.mjs test/artifact-ref-dir-sink.test.mjs test/chat-sticker-guard.test.mjs test/relay-task-progress.test.mjs test/gpw-modes.test.mjs test/joint-cli.test.mjs test/joint-command-hash.test.mjs test/joint-dispatch-audit.test.mjs test/tools-cancel-auth.test.mjs test/fleet-mirror.test.mjs test/sink-git-ssh.test.mjs test/agent-sse-watchdog.test.mjs test/version-gate.test.mjs test/joint-merge-cli.test.mjs test/joint-progress.test.mjs test/project-room-truncation.test.mjs test/chat-crypto-aad.test.mjs test/agent-artifact-delivery.test.mjs test/dataplane-sink-route.test.mjs test/artifact-sink-e2e.test.mjs test/project-room-persistence.test.mjs test/project-room-disk-persistence.test.mjs test/project-stream-route.test.mjs test/chat-v2-gate.test.mjs test/xiangqi.test.mjs test/xfer-foundation.test.mjs test/xfer-ferry.test.mjs test/xfer-publish.test.mjs test/xfer-want-block.test.mjs test/p2p-cipher-diag.test.mjs test/chacha-fallback.test.mjs test/fleet-binding.test.mjs test/xfer-channel.test.mjs test/p2p-race.test.mjs test/path-scheduler.test.mjs test/transport.test.mjs test/iroh-sidecar.test.mjs test/discovery.test.mjs test/p2p-rtt.test.mjs test/artifact-ref-v3.test.mjs test/xfer-recovery.test.mjs test/xfer-router.test.mjs test/remote-s2-capture.test.mjs",
68
70
  "test:acceptance-doc": "node --test --test-force-exit test/acceptance-doc.test.mjs",
69
71
  "test:account-identity": "node --test --test-force-exit test/account-identity.test.mjs",
70
72
  "test:account-join": "node --test --test-force-exit test/account-join.test.mjs",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "twinsearth-w2m-dsh",
4
- "version": "2.0.8",
4
+ "version": "2.0.10",
5
5
  "description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 12 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -69,6 +69,7 @@ import {
69
69
  readExecCapability,
70
70
  } from '../util/exec-capability.mjs';
71
71
  import { CRYPTO_REASON, DEVICE_KEY_FILE, DeviceKey, PeerKeyStore, deriveSigningKey } from './crypto.mjs';
72
+ import { createXferFrameRouter } from './transfer/router.mjs';
72
73
  import { DEFAULT_P2P_MODE, P2P_DEFAULTS, P2PNode, normalizeP2PMode, normalizeP2PPort } from './p2p-node.mjs';
73
74
  import { punchSession, reverseDialSession } from './p2p.mjs';
74
75
  import { createSigner } from '../signing.mjs';
@@ -1142,6 +1143,8 @@ export function createAgent(options) {
1142
1143
  * @type {PeerKeyStore}
1143
1144
  */
1144
1145
  const peerKeys = new PeerKeyStore();
1146
+ /** P0-a: xfer.* frames arriving on a P2P channel are routed here, not to task dispatch. */
1147
+ const xferRouter = createXferFrameRouter();
1145
1148
  /**
1146
1149
  * What each task's direct channel actually was, for the result envelope.
1147
1150
  *
@@ -3959,6 +3962,10 @@ let attemptAttached = false;
3959
3962
  */
3960
3963
  function handleP2PMessage(event) {
3961
3964
  const payload = parseP2PFrame(event.payload);
3965
+ if (payload !== null && typeof payload.type === 'string' && payload.type.startsWith('xfer.')) {
3966
+ xferRouter.handle(payload, { channel: event.channel });
3967
+ return;
3968
+ }
3962
3969
  if (payload === null) {
3963
3970
  log('warn', 'ignoring non-JSON frame on a P2P channel', {
3964
3971
  peer: event.peer ?? null,