@twinsearth/w2m-dsh-plugin 0.0.0-stage → 0.4.6
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/CHANGELOG.md +1288 -0
- package/LICENSE +21 -0
- package/PROTOCOL-v0.1.2.md +165 -0
- package/PROTOCOL-v0.3.0.md +454 -0
- package/PROTOCOL-v0.3.9.md +207 -0
- package/PROTOCOL-v0.4.0.md +255 -0
- package/PROTOCOL.md +339 -0
- package/README.md +665 -2
- package/bin/w2m-localside.mjs +303 -0
- package/bin/w2m-rabbit.mjs +396 -0
- package/bin/w2m-stun.mjs +231 -0
- package/cordis.patch.yml +94 -0
- package/deploy/caddy/Caddyfile.example +118 -0
- package/deploy/docker/Dockerfile +144 -0
- package/deploy/docker/README.md +338 -0
- package/deploy/docker/docker-compose.yml +179 -0
- package/deploy/nginx/w2m.conf.example +196 -0
- package/deploy/shared-server/README.md +118 -0
- package/deploy/shared-server/install.sh +255 -0
- package/deploy/systemd/w2m-rabbit.service +150 -0
- package/deploy/systemd/w2m-stun.service +69 -0
- package/deploy/windows/README.md +115 -0
- package/deploy/windows/install-service.ps1 +195 -0
- package/deploy/windows/w2m-agent-supervisor.mjs +205 -0
- package/deploy/windows/winsw/w2m-localside.xml +50 -0
- package/docs/DEPLOY.md +902 -0
- package/docs/LINT-AUDIT.md +492 -0
- package/docs/PIPELINE-DESIGN.md +95 -0
- package/docs/TROUBLESHOOTING.md +560 -0
- package/docs/UI-CARDS.md +81 -0
- package/lib/index.js +13 -0
- package/lib/tools.js +11 -0
- package/package.json +133 -4
- package/src/agent/agent.mjs +3317 -0
- package/src/agent/allowed-commands.mjs +116 -0
- package/src/agent/caps.mjs +437 -0
- package/src/agent/exec.mjs +356 -0
- package/src/agent/git.mjs +616 -0
- package/src/agent/identity.mjs +253 -0
- package/src/agent/p2p-node.mjs +1243 -0
- package/src/agent/p2p.mjs +605 -0
- package/src/agent/resolve.mjs +148 -0
- package/src/agent/spool.mjs +252 -0
- package/src/agent/stun.mjs +397 -0
- package/src/agent/url.mjs +127 -0
- package/src/plugin/auto-update.mjs +574 -0
- package/src/plugin/schedule.mjs +452 -0
- package/src/plugin/shared-config.mjs +264 -0
- package/src/plugin/tools.mjs +3891 -0
- package/src/plugin/update-install.mjs +764 -0
- package/src/plugin/update-source.mjs +686 -0
- package/src/relay/metrics.mjs +284 -0
- package/src/relay/peers.mjs +180 -0
- package/src/relay/persistence.mjs +207 -0
- package/src/relay/report.mjs +591 -0
- package/src/relay/server.mjs +1187 -0
- package/src/relay/state.mjs +2004 -0
- package/src/relay/stun-server.mjs +733 -0
- package/src/signing.mjs +359 -0
- package/src/util/cli.mjs +70 -0
package/PROTOCOL.md
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# W2M 线协议 v1(冻结)
|
|
2
|
+
|
|
3
|
+
> 项目:**DSH: Windows2MacOS** · 版本 **0.0.1** · 本文件是 Rabbit / Localside / DSH 插件三方共同遵循的**唯一契约**。
|
|
4
|
+
> 任何一方改动本文件即为**破坏性变更**,必须同时升 `protocol_version`。
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 0. 设计约束(不可协商)
|
|
9
|
+
|
|
10
|
+
| 约束 | 含义 |
|
|
11
|
+
|---|---|
|
|
12
|
+
| **零第三方运行时依赖** | 只用 `node:` 内置模块。**禁止** `require`/`import` 任何 npm 包(`ws`、`eventsource` 等一律不用) |
|
|
13
|
+
| **ESM** | 全部 `.mjs` 或 `import` 语法;`package.json` 声明 `"type": "module"` |
|
|
14
|
+
| **传输 = SSE + POST** | 下行 `text/event-stream`,上行 `application/json` 的 POST。**不用 WebSocket**(避免自己写 101 握手与掩码) |
|
|
15
|
+
| **默认只读** | `mode: replicate` 默认 `write: false`;Localside 不得在只读模式下写项目 |
|
|
16
|
+
| **argv 数组** | 命令一律以数组传递,**永不拼接 shell 字符串** |
|
|
17
|
+
| **能力不匹配 → refused** | 绝不静默降级 |
|
|
18
|
+
| **锚不匹配 → unverifiable** | 拒绝汇总,与"结果分歧"分开记账 |
|
|
19
|
+
| **先落盘再回传** | agent 收到任务先写本地 spool,执行完再回传,收到确认才清 |
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 1. 版本与端点
|
|
24
|
+
|
|
25
|
+
`protocol_version = 1`。所有请求/响应均为 UTF-8 JSON。时间一律 **RFC3339 UTC**,哈希一律**小写 hex**。
|
|
26
|
+
|
|
27
|
+
| 方法 | 路径 | 鉴权 | 用途 | 幂等 |
|
|
28
|
+
|---|---|---|---|---|
|
|
29
|
+
| `GET` | `/healthz` | 无 | 存活探针 | — |
|
|
30
|
+
| `POST` | `/v1/pair` | 配对码 | 用一次性配对码换取 `device_token` | 是 |
|
|
31
|
+
| `GET` | `/v1/stream?machine_id=&seq=` | Bearer | SSE 长连(下行事件) | 是(`Last-Event-ID` 可重放) |
|
|
32
|
+
| `POST` | `/v1/heartbeat` | Bearer | 租约续期 + 在线心跳 | 是 |
|
|
33
|
+
| `POST` | `/v1/result` | Bearer | 回传结果信封 | 是(`dedupe_key` 去重) |
|
|
34
|
+
| `POST` | `/v1/task` | Bearer | 提交任务(插件调用) | 否(每次新 `task_id`) |
|
|
35
|
+
| `GET` | `/v1/devices` | Bearer | 设备表 | 是 |
|
|
36
|
+
| `GET` | `/v1/tasks/{task_id}` | Bearer | 任务状态 + 六态聚合 | 是 |
|
|
37
|
+
| `GET` | `/v1/tasks/{task_id}/report?format=md\|json` | Bearer | 汇总报告 | 是 |
|
|
38
|
+
| `GET` | `/v1/tasks?limit=N` | Bearer | 任务列表 | 是 |
|
|
39
|
+
|
|
40
|
+
鉴权头:`Authorization: Bearer <device_token>`。缺失/错误 → `401`,body `{"error":{"code":"UNAUTHORIZED"}}`。
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 2. 设备身份与配对
|
|
45
|
+
|
|
46
|
+
### 2.1 本地身份文件
|
|
47
|
+
`$DSH_HOME/xclient/device.json`(权限 0600):
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"machine_id": "<uuid v4,首次生成后永久不变>",
|
|
51
|
+
"machine_name": "win-desktop",
|
|
52
|
+
"device_token": "<配对成功后写入>",
|
|
53
|
+
"rabbit_url": "http://127.0.0.1:8787"
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
> `machine_id` **不得**用 hostname(hostname 会变、会重名)。
|
|
57
|
+
|
|
58
|
+
### 2.2 配对流程
|
|
59
|
+
```
|
|
60
|
+
1. Rabbit 首次启动生成一次性配对码,打印: PAIR-XXXXXXXX (8 位大写字母数字,24h 有效,成功即失效)
|
|
61
|
+
2. agent POST /v1/pair {pairing_code, machine_id, machine_name, platform, caps}
|
|
62
|
+
3. Rabbit 校验 → 200 {device_token, rabbit_time, protocol_version}
|
|
63
|
+
4. agent 把 device_token 写入 device.json
|
|
64
|
+
```
|
|
65
|
+
`POST /v1/pair` 请求:
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"pairing_code": "PAIR-7K2M9QX4",
|
|
69
|
+
"machine_id": "3f2a…",
|
|
70
|
+
"machine_name": "win-desktop",
|
|
71
|
+
"platform": { "os": "windows", "os_version": "10.0.26100", "arch": "x64",
|
|
72
|
+
"shell": "pwsh", "node": "v24.21.0" },
|
|
73
|
+
"caps": { "case_sensitive_fs": false, "symlinks": false, "exec_bit": false,
|
|
74
|
+
"python": null, "npm": null, "node": "v24.21.0" },
|
|
75
|
+
"user_id": "<可选;本机 DeepSeek 账号 userId,用于同账号房间准入>"
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
响应:`{ "device_token": "…", "protocol_version": 1, "rabbit_time": "…", "next_pairing_code": "PAIR-…" }`
|
|
79
|
+
|
|
80
|
+
**配对码的生命周期(v0.0.1 修订)**:一个组不止一台机器,而初稿的"一次性配对码"会让运维**无法配第二台机器**(除非重启中继)—— 这是实测发现的可运维性缺陷。现行为:
|
|
81
|
+
1. 某码被成功使用后**立即作废并自动生成新码**,新码在响应 `next_pairing_code`、SSE `notice/PAIRING_CODE_ROTATED`、以及中继 stdout 上同时给出;
|
|
82
|
+
2. 运维的典型流程是:配机器 A → 看到新码 → 用新码配机器 B;
|
|
83
|
+
3. 需要"一个码配完整个窗口"(自动化场景,没人在看 stdout)时,用 `pairingCodeReusable: true` 启动中继,此时该码在 TTL 内可重复使用;
|
|
84
|
+
4. `--pairing-code <c>` 可指定固定码,但**仍然是单次 + 轮换**语义。
|
|
85
|
+
|
|
86
|
+
注意:`POST /v1/pair` 用同一个 `machine_id` 再次配对会**吊销该机器上一个 token**(防重放),`re_paired` 标记在设备表里可查。
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 3. SSE 事件流
|
|
91
|
+
|
|
92
|
+
### 3.1 帧格式
|
|
93
|
+
标准 SSE:`event: <type>` + `data: <json>` + 空行。服务端同时发 `id: <seq>`,客户端断线重连时带 `?seq=<last+1>` 或 `Last-Event-ID` 头。
|
|
94
|
+
|
|
95
|
+
**首帧必须是 `ready`**,客户端在收到 `ready` 之前**不得**认为已连接(照抄 DSH 自身 `$events` 的语义:先 ready 再增量)。
|
|
96
|
+
|
|
97
|
+
### 3.2 事件类型
|
|
98
|
+
| `type` | 字段 | 说明 |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `ready` | `seq, protocol_version, rabbit_time, machine_id` | 连接就绪;此后才开始增量 |
|
|
101
|
+
| `task.offer` | 见 §4.2 | 下发任务 |
|
|
102
|
+
| `task.cancel` | `task_id, reason` | 取消 |
|
|
103
|
+
| `task.result` | `task_id, dedupe_key, attempt, index, status, machine_id, deduped, validation_errors[]` | **v0.0.1 追加**:某台机器回传了结果。初稿遗漏了"结果到达"事件,导致插件只能轮询。客户端**必须忽略未知 `type`**,因此追加是向后兼容的 |
|
|
104
|
+
| `peer.hello` | `machine_id, machine_name, platform` | 有新设备上线 |
|
|
105
|
+
| `peer.bye` | `machine_id, reason` | 设备离线 |
|
|
106
|
+
| `notice` | `level: info\|warn\|error, code, message` | 服务端通告。`code` 见下表 |
|
|
107
|
+
|
|
108
|
+
**`notice.code` 取值**:`PAIRING_CODE_ROTATED`(配对码已轮换,新码在 `pairing_code` 字段)、`REPLAY_TRUNCATED`(请求的 `seq` 已超出环形缓冲窗口)、`LEASE_TAKEOVER_UNAVAILABLE`(有 index 需要接管但没有空闲机器)。
|
|
109
|
+
|
|
110
|
+
每条事件都带 `seq`(**单调递增整数,从 1 开始,进程生命周期内唯一**)。SSE 保活:每 **15s** 发一行注释 `: keepalive`。
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 4. 任务与投递
|
|
115
|
+
|
|
116
|
+
### 4.1 `POST /v1/task` 请求
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"mode": "replicate",
|
|
120
|
+
"command_argv": ["node", "--test"],
|
|
121
|
+
"cwd_rel": ".",
|
|
122
|
+
"index_total": 1,
|
|
123
|
+
"timeout_ms": 300000,
|
|
124
|
+
"write": false,
|
|
125
|
+
"write_scope": ["src/"],
|
|
126
|
+
"require_exclusive_write": false,
|
|
127
|
+
"base_commit": "9f1c…",
|
|
128
|
+
"base_tree": "4b8e…",
|
|
129
|
+
"requirements": { "toolchain": { "node": ">=20" }, "platform": ["windows","macos"] },
|
|
130
|
+
"compare_policy": { "strip_ansi": true, "normalize_crlf": true,
|
|
131
|
+
"strip_trailing_blank_lines": true, "redact": [] },
|
|
132
|
+
"halt": "never",
|
|
133
|
+
"created_by": "win-desktop"
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
响应:`{ "task_id": "<ULID>", "leases": [ {"machine_id":"…","index":0,"state":"queued"} ], "seq": 42 }`
|
|
137
|
+
|
|
138
|
+
**任务发给谁(初稿未定义,v0.0.1 补充)**:可选字段 `target_machines: ["<machine_id>", …]`。
|
|
139
|
+
- 给了 → 只发给列出的机器;
|
|
140
|
+
- 没给 → 发给**全部已配对设备**。
|
|
141
|
+
|
|
142
|
+
**index 如何分配**:`replicate` → 所有机器 `index = 0`、`index_total = 1`;`split` → 按设备顺序 `index = i % index_total`。
|
|
143
|
+
能力门失败的机器仍会拿到一条 `state: "refused"` 的 lease,但**不会收到 offer**。
|
|
144
|
+
|
|
145
|
+
### 4.1.1 `GET /v1/tasks/{task_id}` 响应形状(初稿未定义,v0.0.1 补充)
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"protocol_version": 1,
|
|
149
|
+
"rabbit_time": "2026-10-07T12:00:00Z",
|
|
150
|
+
"task": { "task_id": "…", "mode": "replicate", "index_total": 1, "base_commit": "…", "created_at": "…" },
|
|
151
|
+
"leases": [ { "machine_id": "…", "index": 0, "attempt": 1, "state": "done" } ],
|
|
152
|
+
"aggregate": {
|
|
153
|
+
"status": "consistent",
|
|
154
|
+
"machines": [
|
|
155
|
+
{ "machine_id": "…", "machine_name": "win-desktop", "index": 0, "attempt": 1,
|
|
156
|
+
"lease_state": "done", "outcome": "ok", "status": "ok",
|
|
157
|
+
"refusal_reason": null, "exit_code": 0, "reasons": [] }
|
|
158
|
+
],
|
|
159
|
+
"steps": [ { "step": 0, "name": "capability gate", "refused": [] }, "…" ],
|
|
160
|
+
"differences": [],
|
|
161
|
+
"counts": { "refused": 0, "unverifiable": 0, "ok": 2, "failed": 0, "expired": 0, "pending": 0 },
|
|
162
|
+
"notes": [],
|
|
163
|
+
"expected": { "base_commit": "…", "base_tree": "…", "command_hash": "…" }
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
- **终态判定读 `aggregate.status`**(不是顶层 `status`,也没有顶层 `aggregate` 字符串)。
|
|
168
|
+
- `machines[].outcome` 是**每台机器**的判定,`machines[].reasons[]` 是它被扣分的具体理由(例如 `"pre_tree_fingerprint != base_tree"`)。
|
|
169
|
+
- `aggregate` 在结果到达前是 `pending`。**`pending` 与"某台机器仍 pending"都不算终态**:初稿的六态里 `partial` 会在第一台结果落地时立刻出现,此时其余机器仍在跑 —— 消费者必须同时检查 `machines[].outcome === "pending"`。
|
|
170
|
+
- 六态之外还有两个仅作展示的值:`pending`(首条结果到达前)、`refused`(所有 lease 都被能力门拒绝)。
|
|
171
|
+
|
|
172
|
+
### 4.2 `task.offer` 事件
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"type": "task.offer",
|
|
176
|
+
"seq": 43,
|
|
177
|
+
"task_id": "01J…",
|
|
178
|
+
"attempt": 1,
|
|
179
|
+
"mode": "replicate",
|
|
180
|
+
"index": 0,
|
|
181
|
+
"index_total": 1,
|
|
182
|
+
"command_argv": ["node", "--test"],
|
|
183
|
+
"cwd_rel": ".",
|
|
184
|
+
"write": false,
|
|
185
|
+
"timeout_ms": 300000,
|
|
186
|
+
"base_commit": "9f1c…",
|
|
187
|
+
"base_tree": "4b8e…",
|
|
188
|
+
"requirements": { },
|
|
189
|
+
"compare_policy": { },
|
|
190
|
+
"dedupe_key": "<sha256>",
|
|
191
|
+
"deadline": "2026-10-07T12:00:00Z"
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### 4.3 租约(长租约 + 进度心跳 —— **不是固定超时**)
|
|
196
|
+
```
|
|
197
|
+
agent 认领后每 10s POST /v1/heartbeat {task_id, machine_id, attempt, phase, progress}
|
|
198
|
+
phase ∈ {preparing, running, finalizing}
|
|
199
|
+
Rabbit 收到心跳即续期;连续 2 个心跳周期(20s)+ grace 30s 未收到 → 该 lease 判 expired
|
|
200
|
+
→ halt=never 时仅记账并标 partial;halt=now 时立即通知其他机器可接管
|
|
201
|
+
租约到期判断**由 Rabbit 的时间裁定**,agent 不得自行判断(避免时钟漂移导致双主)
|
|
202
|
+
```
|
|
203
|
+
响应:`{ "lease_until": "…", "cancel": false }` —— `cancel: true` 表示该任务已被取消,agent 应立即停止。
|
|
204
|
+
|
|
205
|
+
### 4.4 去重(**v0.0.1 修订:幂等键是三元组**)
|
|
206
|
+
```
|
|
207
|
+
dedupe_key = sha256(task_id + "|" + index + "|" + command_hash + "|" + base_tree)
|
|
208
|
+
幂等键 = (machine_id, dedupe_key, attempt)
|
|
209
|
+
```
|
|
210
|
+
> ⚠️ **这是对本协议初稿的一处修正,实现必须照此。** 初稿写"重复投递命中已有结果即去重",但 `dedupe_key` 里**没有 `machine_id`**,于是 `replicate` 模式下所有机器算出**同一个** key —— 严格按它去重会把除第一台外**所有机器的结果静默丢弃**,§6.3 的跨机聚合直接不可能。修正后的行为:
|
|
211
|
+
> - 同一机器 + 同一 `attempt` 重复 POST → `200 {"deduped": true}`,账本不新增行
|
|
212
|
+
> - 不同机器 → 结果全部入库
|
|
213
|
+
> - 同一机器 + 更大 `attempt`(中继重新投递 = 真重试)→ 视为新一次执行,入库
|
|
214
|
+
|
|
215
|
+
**两条重传语义(易错,务必区分)**:
|
|
216
|
+
|
|
217
|
+
| 场景 | `attempt` | 结果 |
|
|
218
|
+
|---|---|---|
|
|
219
|
+
| 传输失败重发(网络抖动,重发**同一个**结果) | **不变** | `deduped: true` |
|
|
220
|
+
| 中继因租约过期/接管而**重新投递** | **+1** | 必须真正重跑(缓存键含 `attempt`) |
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## 5. 结果信封
|
|
225
|
+
|
|
226
|
+
`POST /v1/result` body = 完整信封。UTF-8 JSON,字段 snake_case。**未知字段必须被忽略**(向前兼容)。
|
|
227
|
+
|
|
228
|
+
### 5.1 必需字段(缺一即 `unverifiable`)
|
|
229
|
+
| 字段 | 类型 | 语义 |
|
|
230
|
+
|---|---|---|
|
|
231
|
+
| `envelope_version` | string | `"1.0"` |
|
|
232
|
+
| `task_id` | string | ULID |
|
|
233
|
+
| `attempt` | int ≥1 | 第几次投递 |
|
|
234
|
+
| `dedupe_key` | string | 见 §4.4 |
|
|
235
|
+
| `machine_id` / `machine_name` | string | 身份 |
|
|
236
|
+
| `platform` | object | `{os, os_version, arch, shell, shell_version}` |
|
|
237
|
+
| `caps` | object | 能力声明 |
|
|
238
|
+
| `index` / `index_total` | int | replicate 固定 0/1 |
|
|
239
|
+
| `mode` | `replicate`\|`split` | |
|
|
240
|
+
| `cwd_rel` | string | 相对项目根,正斜杠 |
|
|
241
|
+
| `base_commit` | string | 中继广播 |
|
|
242
|
+
| `base_tree` | string\|null | 期望工作区指纹 |
|
|
243
|
+
| `pre_tree_fingerprint` | string\|null | 执行前实测 |
|
|
244
|
+
| `post_tree_fingerprint` | string\|null | 执行后实测 |
|
|
245
|
+
| `fingerprint_algo` | string | `git-temp-index-tree/v1` |
|
|
246
|
+
| `fingerprint_error` | string\|null | 失败原因码 |
|
|
247
|
+
| `head_commit` | string\|null | 结束时 HEAD |
|
|
248
|
+
| `dirty_before` | bool | 执行前是否 dirty |
|
|
249
|
+
| `command_argv` | string[] | 实际 argv |
|
|
250
|
+
| `command_hash` | string | `sha256(JCS(argv)+"|"+shell_id+"|"+cwd_rel)` |
|
|
251
|
+
| `shell_id` | string | `direct-exec`(v0.0.1 只用直执行) |
|
|
252
|
+
| `started_at` / `ended_at` | string | RFC3339 UTC |
|
|
253
|
+
| `duration_ms` | int | 墙钟 |
|
|
254
|
+
| `exit_code` | int\|null | 被信号杀死为 null |
|
|
255
|
+
| `status` | enum | `ok\|nonzero_exit\|timeout\|crashed\|refused\|unverifiable` |
|
|
256
|
+
| `refusal_reason` | string\|null | `status=refused` 时必填 |
|
|
257
|
+
| `stdout_sha256` / `stdout_bytes` | string / int | **原始字节流** |
|
|
258
|
+
| `stderr_sha256` / `stderr_bytes` | string / int | 同上 |
|
|
259
|
+
| `warnings` | string[] | 告警码 |
|
|
260
|
+
| `envelope_sha256` | string | 除本字段外 JCS 规范化后哈希 |
|
|
261
|
+
|
|
262
|
+
### 5.2 可选字段(有则参与比对)
|
|
263
|
+
`stdout_normalized_sha256`、`stdout_head`/`stdout_tail`(各 4 KiB,**不参与比对**)、`artifacts[{path,algo,hash,size}]`、`diff_numstat[{path,added,deleted,is_binary}]`、`untracked[]`、`tests{runner,total,passed,failed,skipped}`、`semantic_counts{}`、`toolchain{}`、`lockfiles[]`、`unpinned_deps`、`submodules[]`、`lfs`、`signal`。
|
|
264
|
+
|
|
265
|
+
### 5.3 可比字段(只有这些参与一致性判定)
|
|
266
|
+
`task_id, index, index_total, mode, cwd_rel, base_commit, base_tree, pre_tree_fingerprint, fingerprint_algo, fingerprint_error, head_commit, command_hash, shell_id, exit_code, status, refusal_reason, stdout_sha256, stdout_bytes, stdout_normalized_sha256, stderr_sha256, artifacts, diff_numstat, tests, semantic_counts, warnings`
|
|
267
|
+
|
|
268
|
+
### 5.4 告警码
|
|
269
|
+
`CASE_COLLISION`、`CRLF_DRIFT`、`SUBMODULE_DIRTY`、`LFS_MISSING`、`PATH_INVALID`、`INDEX_LOCK`、`NONDETERMINISTIC_OUTPUT`、`DIRTY_WORKTREE`、`FINGERPRINT_UNAVAILABLE`
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## 6. 能力门与聚合判定
|
|
274
|
+
|
|
275
|
+
### 6.1 能力门(下发**前**,逐机)
|
|
276
|
+
```
|
|
277
|
+
若 task.requirements.toolchain 中某项该机不满足(如 node 版本)→ 该机 status = refused, refusal_reason = MISSING_<X>
|
|
278
|
+
若 task.requirements.platform 非空且不含该机 os → refused, PLATFORM_MISMATCH
|
|
279
|
+
若 task.write == true 且该机 caps 不支持写(只读挂载等) → refused, READ_ONLY_MACHINE
|
|
280
|
+
```
|
|
281
|
+
`refused` **不是失败**,是"拒绝执行",单独记账。
|
|
282
|
+
|
|
283
|
+
### 6.2 三锚(一致性判定的前提)
|
|
284
|
+
`base_commit` + `pre_tree_fingerprint` + `command_hash`。
|
|
285
|
+
> `toolchain` / `platform` **不是锚**,它们走能力门。
|
|
286
|
+
|
|
287
|
+
### 6.3 六态聚合(Rabbit 侧)
|
|
288
|
+
```
|
|
289
|
+
Step 0 任一机器 refused → 该机记 refused(不阻断其他机器)
|
|
290
|
+
Step 1 任一机器缺必需字段 或 base_commit≠期望 或
|
|
291
|
+
pre_tree_fingerprint≠base_tree 或 command_hash 不一致 → unverifiable(拒绝汇总)
|
|
292
|
+
Step 2 其余机器中:
|
|
293
|
+
全部 ok → 进 Step 3
|
|
294
|
+
部分 ok、部分 {nonzero_exit,timeout,crashed} → partial
|
|
295
|
+
全部 {nonzero_exit,timeout,crashed} → failed
|
|
296
|
+
Step 3 比对全部"可比字段":
|
|
297
|
+
全等 → consistent
|
|
298
|
+
仅 toolchain/platform 不同且差异集中在 stdout 文本 → divergent-platform(期望内,不告警)
|
|
299
|
+
其他差异 → divergent(列出字段与逐机取值)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## 7. 错误响应统一形状
|
|
305
|
+
```json
|
|
306
|
+
{ "error": { "code": "UNAUTHORIZED", "message": "…", "detail": { } } }
|
|
307
|
+
```
|
|
308
|
+
码表:`UNAUTHORIZED`、`BAD_REQUEST`、`NOT_FOUND`、`PAIRING_INVALID`、`PAIRING_EXPIRED`、`TASK_EXISTS`、`FRAME_TOO_LARGE`、`NO_ONLINE_DEVICE`、`INTERNAL`。
|
|
309
|
+
|
|
310
|
+
### 7.1 `refusal_reason` 码表(v0.0.1 补充)
|
|
311
|
+
| 码 | 含义 |
|
|
312
|
+
|---|---|
|
|
313
|
+
| `COMMAND_NOT_ALLOWED` | argv 不在该机的 default-deny 白名单内 |
|
|
314
|
+
| `MISSING_<TOOL>` | 例如 `MISSING_PYTHON`、`MISSING_NPM`:能力门要求该工具,但该机探不到 |
|
|
315
|
+
| `PLATFORM_MISMATCH` | 任务 `requirements.platform` 不含该机 OS |
|
|
316
|
+
| `READ_ONLY_MACHINE` | 任务 `write: true`,但该机项目目录不可写(`fs.access(W_OK)` 失败) |
|
|
317
|
+
| `CWD_OUTSIDE_PROJECT` | 任务 `cwd_rel` 解析后逃出项目根 |
|
|
318
|
+
| `INVALID_OFFER` | offer 结构非法(缺字段/类型错) |
|
|
319
|
+
| `POLICY_VIOLATION` | 其他策略拒绝(如声明 `write_intent: none` 却尝试写) |
|
|
320
|
+
|
|
321
|
+
### 7.2 `fingerprint_error` 码表(v0.0.1 补充)
|
|
322
|
+
`GIT_MISSING`、`NOT_A_REPO`、`NO_HEAD`、`READ_TREE_FAILED`、`ADD_FAILED`、`WRITE_TREE_FAILED`、`GIT_TIMEOUT`、`TEMP_INDEX_FAILED`、`BAD_TREE`。
|
|
323
|
+
|
|
324
|
+
出现任一 `fingerprint_error` → 该机 `pre_tree_fingerprint` 无法采信 → **`unverifiable`**(而不是"分歧")。
|
|
325
|
+
|
|
326
|
+
### 7.3 告警码补充(v0.0.1)
|
|
327
|
+
除 §5.4 已列的码外,实测两端都会同样产生的两个:`OUTPUT_TRUNCATED`(stdout/stderr 超过 2 MiB 保留上限,**注意:`stdout_sha256` 覆盖的是完整原始流,不是被截断的部分**)、`AGENT_ERROR`(agent 内部错误);另加 `DIRTY_WORKTREE`。
|
|
328
|
+
|
|
329
|
+
### 7.4 不完整信封的处理(v0.0.1 明确)
|
|
330
|
+
`POST /v1/result` 收到**缺必需字段**的信封时 **返回 200 并入库**,聚合时记 `unverifiable` —— **不拒收**。理由:拒绝接收等于把"这台机器跑完了但报告不完整"变成"这台机器没有结果",那是更严重的失真。`validation_errors[]` 会随 `task.result` 事件一并发给观察者。
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## 8. v0.0.1 明确不做(避免范围蔓延)
|
|
335
|
+
- 不做 WebSocket、不做 TLS 终结(由用户前置反代或内网承担)
|
|
336
|
+
- 不做自动负载均衡切片(只做 `explicit` / `modulo`)
|
|
337
|
+
- 不做写回合并(`write: true` 时仅记录分支名,不做 merge)
|
|
338
|
+
- 不做跨机浏览器看板(那是设计文档的 P6)
|
|
339
|
+
- 不做 Mac 真机验证(本机无 Mac;代码跨平台但需用户在 Mac 上复验)
|