@twinsearth/w2m-dsh-plugin 2.0.4 → 2.0.5
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/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +8 -0
- package/bin/w2m-joint.mjs +69 -7
- package/codex/mcp-servers.toml +1 -1
- package/docs/acceptance/2.0.5/RELEASE-205.json +21 -0
- package/docs/acceptance/2.0.5/RELEASE-2050.json +22 -0
- package/docs/adr/0001-collaboration-crdt.md +59 -0
- package/lib/client.js +146 -8
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/src/agent/agent.mjs +97 -36
- package/src/agent/artifact-delivery.mjs +89 -0
- package/src/agent/caps.mjs +16 -2
- package/src/agent/sinks/git-sink.mjs +78 -0
- package/src/agent/sinks/ssh-sink.mjs +83 -0
- package/src/agent/sse-cursor.mjs +16 -0
- package/src/agent/sse-watchdog.mjs +71 -0
- package/src/plugin/dataplane-sink-route.mjs +113 -0
- package/src/plugin/fleet-chat-route.mjs +11 -0
- package/src/plugin/joint-dev/dispatch.mjs +5 -1
- package/src/plugin/joint-dev/merge-approval.mjs +115 -0
- package/src/plugin/joint-dev/progress.mjs +107 -0
- package/src/plugin/joint-dev/relay-dispatch.mjs +52 -22
- package/src/plugin/project-stream-route.mjs +58 -0
- package/src/plugin/tools-joint.mjs +6 -1
- package/src/plugin/tools.mjs +25 -5
- package/src/relay/fleet-messages.mjs +48 -24
- package/src/relay/project-room.mjs +47 -2
- package/src/relay/server.mjs +11 -5
- package/src/relay/state.mjs +63 -3
- package/src/util/chat-crypto.mjs +70 -1
- package/docs/COLLAB/COLLABORATION-MODEL.md +0 -240
- package/docs/COLLAB/EXECUTION-AUTH-MODEL.md +0 -361
- package/docs/COLLAB/M1-ACCEPTANCE-RUNBOOK.md +0 -70
- package/docs/COLLAB/M1-MAINTENANCE.md +0 -313
- package/docs/COLLAB/M2-TASK-GRANT.md +0 -231
- package/docs/COLLAB/M3-GIT-NATIVE-TASKS.md +0 -291
- package/docs/COLLAB/M4-PROJECT-ROOM.md +0 -260
- package/docs/COLLAB/M5-MULTI-CLIENT-PRIMITIVES.md +0 -263
- package/docs/COLLAB/M6-CROSS-NETWORK-RELIABILITY.md +0 -231
- package/docs/COLLAB/M7-METRICS-ACCEPTANCE.md +0 -263
- package/docs/COLLAB/TASK-MODEL.md +0 -281
- package/docs/acceptance/1.9.10/RELEASE-1910.json +0 -21
- package/docs/acceptance/1.9.11/RELEASE-1911.json +0 -20
- package/docs/acceptance/1.9.12/RELEASE-1912.json +0 -20
- package/docs/acceptance/1.9.13/RELEASE-1913.json +0 -20
- package/docs/acceptance/1.9.14/RELEASE-1914.json +0 -20
- package/docs/acceptance/1.9.15/RELEASE-1915.json +0 -22
- package/docs/acceptance/1.9.16/RELEASE-1916.json +0 -21
- package/docs/acceptance/1.9.17/RELEASE-1917.json +0 -20
- package/docs/acceptance/1.9.19/RELEASE-1919.json +0 -21
- package/docs/acceptance/1.9.20/RELEASE-1920.json +0 -22
- package/docs/acceptance/1.9.4/RELEASE-194.json +0 -23
- package/docs/acceptance/1.9.5/RELEASE-195.json +0 -23
- package/docs/acceptance/1.9.6/RELEASE-196.json +0 -22
- package/docs/acceptance/1.9.7/RELEASE-197.json +0 -21
- package/docs/acceptance/1.9.8/RELEASE-198.json +0 -21
- package/docs/acceptance/1.9.9/RELEASE-199.json +0 -21
- package/lib/client.js.bak2 +0 -640
- package/src/plugin/dataflow.mjs +0 -72
- package/src/plugin/fleet-target.mjs.bak-arch2 +0 -812
- package/src/plugin/fleet-target.mjs.bak-archive +0 -812
- package/src/plugin/tofuture/report.mjs +0 -44
- package/src/plugin/tools.mjs.bak-c7 +0 -7029
- package/src/relay/tofuture-whitelist.mjs +0 -46
- package/src/util/data-plane.mjs +0 -180
- package/src/util/sealed-object.mjs +0 -141
- package/src/util/tofuture-hmac.mjs +0 -30
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
# M1 运维通道(v1.3.2-beta)设计规格
|
|
2
|
-
|
|
3
|
-
> 状态:**设计稿**。本文只定义"怎么做、边界在哪、如何验收",不含实现。
|
|
4
|
-
> 上游:`docs/MULTI-MACHINE-COLLABORATION-PLAN.md` 的 M1。
|
|
5
|
-
> 版本:`1.3.2-beta.*`(beta 默认走 GitHub prerelease,不入 npm)。
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## 1. 目标与非目标
|
|
10
|
-
|
|
11
|
-
### 目标(本版本必须做到)
|
|
12
|
-
|
|
13
|
-
1. **远程升级**:操作员不登录机器,即可把编队里任意一台机器上的本项目插件 / relay / agent 升级到指定版本,并完成重启。
|
|
14
|
-
2. **远程重启**:按固定顺序重启本项目的 systemd 单元(Linux)或服务(Windows),不依赖 SSH。
|
|
15
|
-
3. **远程诊断**:收集一组**预先定死**的只读诊断信息(版本、服务状态、日志尾部、磁盘、端口),结果回传 relay。
|
|
16
|
-
4. **无需生前 allow-list**:目标机器即使 allow-list 为空,也能完成上述操作(这是本版本存在的理由)。
|
|
17
|
-
5. **可审计**:每次请求有签名、有发起者、有 hash、有结果,落在 relay 事件日志与机器本地日志里,事后可追。
|
|
18
|
-
|
|
19
|
-
### 非目标(明确不做,避免范围蔓延)
|
|
20
|
-
|
|
21
|
-
- 不做任意命令执行(那是 M2 的"任务级授权"领域)。
|
|
22
|
-
- 不做文件下发/上传(诊断结果走既有 blob 通道,不新增文件传输语义)。
|
|
23
|
-
- 不接管 DSH 自身的安装/升级(只处理**本插件**及其 relay/agent 服务;DSH 本体升级属于另一条线)。
|
|
24
|
-
- 不实现 Windows 之外的平台(含 macOS)在 1.3.2 的范围;Windows 用"计划任务 + 队列目录"的等价设计,macOS 放 1.3.3 之后(见 §10)。
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## 2. 拓扑与信任边界
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
操作员(控制端 DSH 插件)
|
|
32
|
-
│ 1. 签名 maintenance request(operator 私钥)
|
|
33
|
-
▼
|
|
34
|
-
relay(可信传输,但不可信为授权方)
|
|
35
|
-
│ 2. 作为普通 task 的附件下发(maintenance_request 字段),或本地投放
|
|
36
|
-
▼
|
|
37
|
-
机器上的 agent(非特权用户,如 w2m)
|
|
38
|
-
│ 3. 只做一件事:把已验证形状的 request 写成文件放进队列目录
|
|
39
|
-
▼
|
|
40
|
-
队列目录 /var/lib/w2m/maintenance/queue(root 可写,agent 只可写文件、不可执行)
|
|
41
|
-
│ 4. root/SYSTEM 权限的独立单元(path unit 或 timer)轮询
|
|
42
|
-
▼
|
|
43
|
-
维护执行器(固定 action:update / restart / diag)
|
|
44
|
-
│ 5. 验签 → 校验 action 参数 → 执行 → 写结果
|
|
45
|
-
▼
|
|
46
|
-
结果目录 /var/lib/w2m/maintenance/done
|
|
47
|
-
│ 6. agent 读取结果 → 作为 task 结果回报 relay
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
**信任边界**:
|
|
51
|
-
|
|
52
|
-
| 主体 | 能做什么 | 不能做什么 |
|
|
53
|
-
|---|---|---|
|
|
54
|
-
| 操作员私钥 | 签发 maintenance request | 不直接接触目标机 shell |
|
|
55
|
-
| relay | 转发、存 hash、记账 | 不能伪造 request(无操作员私钥);不能改 action 参数(hash 绑定) |
|
|
56
|
-
| agent(w2m 用户) | 写 request 文件、读 done 文件 | 不能执行维护动作、不能改 root 单元、不能提权 |
|
|
57
|
-
| root 维护单元 | 执行白名单 action、验证签名 | 不解释用户输入、不跑任意 shell/脚本、不接受路径穿越 |
|
|
58
|
-
| 其他编队成员 | 看到任务/结果 | 无法代签、无法重放 |
|
|
59
|
-
|
|
60
|
-
---
|
|
61
|
-
|
|
62
|
-
## 3. 请求协议(Request Envelope)
|
|
63
|
-
|
|
64
|
-
### 3.1 文件格式
|
|
65
|
-
|
|
66
|
-
请求文件为 UTF-8 JSON,**单行、≤ 64 KiB**,文件名 `<unix_ms>-<request_id>.json`:
|
|
67
|
-
|
|
68
|
-
```json
|
|
69
|
-
{
|
|
70
|
-
"protocol": "w2m-maintenance/v1",
|
|
71
|
-
"request_id": "01M6...26 字符 ULID",
|
|
72
|
-
"action": "update",
|
|
73
|
-
"params": {
|
|
74
|
-
"target": "plugin",
|
|
75
|
-
"version": "1.3.2-beta.1",
|
|
76
|
-
"artifact_sha256": "64 hex",
|
|
77
|
-
"artifact_urls": ["https://registry.npmjs.org/...tgz", "https://github.com/.../....tgz"],
|
|
78
|
-
"restart_after_update": true
|
|
79
|
-
},
|
|
80
|
-
"machine_id": "目标机器 machine_id(必须完全匹配)",
|
|
81
|
-
"nonce": "16-64 字符随机串",
|
|
82
|
-
"issued_at": "2026-10-21T13:00:00Z",
|
|
83
|
-
"expires_at": "2026-10-21T13:10:00Z",
|
|
84
|
-
"issued_by": "operator 名(仅审计展示,不参与授权)",
|
|
85
|
-
"signature": {
|
|
86
|
-
"alg": "ed25519",
|
|
87
|
-
"key_id": "operator 公钥指纹前 16 hex",
|
|
88
|
-
"value_base64": "签名值"
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
### 3.2 签名覆盖范围
|
|
94
|
-
|
|
95
|
-
签名对象是**规范化 JSON**(键按字典序、无空白、UTF-8),覆盖除 `signature` 外的所有字段:
|
|
96
|
-
|
|
97
|
-
```
|
|
98
|
-
canonical = JSON.stringify(sortKeys({protocol, request_id, action, params, machine_id, nonce, issued_at, expires_at, issued_by}))
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
规则:
|
|
102
|
-
|
|
103
|
-
- 校验方**重新规范化**,不信任文件里的字段顺序。
|
|
104
|
-
- `action` 与 `params` 都在签名内 → relay 或传输层改不了动作与参数。
|
|
105
|
-
- `machine_id` 在签名内 → 请求不能换机重放。
|
|
106
|
-
- `expires_at` 在签名内 → 操作员给的时限不可延长。
|
|
107
|
-
|
|
108
|
-
### 3.3 校验顺序(fail-closed,任何一步失败都拒绝并写结果)
|
|
109
|
-
|
|
110
|
-
```
|
|
111
|
-
1. 文件大小/形状/协议版本
|
|
112
|
-
2. 签名验证(操作员公钥,key_id 找公钥;未知 key_id 拒绝)
|
|
113
|
-
3. machine_id 与本地一致
|
|
114
|
-
4. nonce 未使用(本地已执行记录 + 结果目录去重;跨重启保留)
|
|
115
|
-
5. request_id 未执行过(幂等)
|
|
116
|
-
6. 时间:issued_at ≤ now ≤ expires_at,且 expires_at - issued_at ≤ 10 分钟
|
|
117
|
-
7. action ∈ {update, restart, diag},params 按 action 白名单逐字段校验
|
|
118
|
-
8. exec 前再次检查 artifact sha256 与 action 约束(TOCTOU 防护)
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
---
|
|
122
|
-
|
|
123
|
-
## 4. 动作规格(Action Semantics)
|
|
124
|
-
|
|
125
|
-
### 4.1 `update`
|
|
126
|
-
|
|
127
|
-
参数:`target ∈ {plugin, relay, agent}`、`version`、`artifact_sha256`、`artifact_urls[]`(最多 3 个,按顺序尝试)、`restart_after_update`。
|
|
128
|
-
|
|
129
|
-
步骤(全部固定、无 shell 拼接):
|
|
130
|
-
|
|
131
|
-
1. 下载 artifact 到临时文件(仅 https;每个 URL 超时 60s;总大小上限 200 MiB)。
|
|
132
|
-
2. 计算 sha256,必须与参数完全一致,否则删文件并失败(`UPDATE_HASH_MISMATCH`)。
|
|
133
|
-
3. **当前版本备份**:目录软链/时间戳副本(`/opt/w2m/app.old`),保留最近 1 份。
|
|
134
|
-
4. 解包到 staging 目录,校验包内 `package.json.version === version`(防"hash 对但版本错")。
|
|
135
|
-
5. 原子换入(rename),保留旧树。
|
|
136
|
-
6. 如 `restart_after_update=true` → 执行 restart 动作(同一套固定流程)。
|
|
137
|
-
7. **健康检查**:本机服务 `is-active` + relay `/healthz` 200 + 版本回报;失败则**自动回滚**到旧树并重启,结果中标记 `rolled_back: true`。
|
|
138
|
-
8. 写结果文件(成功/失败/回滚、耗时、版本、sha)。
|
|
139
|
-
|
|
140
|
-
### 4.2 `restart`
|
|
141
|
-
|
|
142
|
-
参数:`targets ∈ {rabbit, agent, all}`、`wait_healthy`(默认 true)。
|
|
143
|
-
|
|
144
|
-
固定顺序(Linux):
|
|
145
|
-
|
|
146
|
-
```
|
|
147
|
-
rabbit(relay)→ 等待 /healthz 200(≤60s)
|
|
148
|
-
agent(localside)→ 等待心跳上报(≤120s,经 relay 可观测)
|
|
149
|
-
stun 可选 → 等待端口监听
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
禁止:重启任意其他单元;禁止 `systemctl restart` 参数拼接。
|
|
153
|
-
|
|
154
|
-
### 4.3 `diag`
|
|
155
|
-
|
|
156
|
-
固定只读清单(输出上限每项 32 KiB,总计 ≤ 256 KiB):
|
|
157
|
-
|
|
158
|
-
- `version`:插件/relay/agent 的 package.json 版本、安装路径、文件 mtime
|
|
159
|
-
- `services`:各单元 `is-active`、`MainPID`、最近一次启动时间
|
|
160
|
-
- `journal`:每个单元最近 100 行(截断)
|
|
161
|
-
- `disk`:`df -h` 关键路径
|
|
162
|
-
- `ports`:本机监听中与 w2m 相关的端口
|
|
163
|
-
- `allowlist`:当前 allow-list(只报告,不改动)
|
|
164
|
-
- `relay`:本地缓存的 relay 可达性/healthz 摘要
|
|
165
|
-
|
|
166
|
-
结果作为文本 + JSON 写入结果目录,agent 以 blob 回报(超过内联阈值自动走 blob 通道)。
|
|
167
|
-
|
|
168
|
-
---
|
|
169
|
-
|
|
170
|
-
## 5. 执行单元(Privileged Runner)
|
|
171
|
-
|
|
172
|
-
### 5.1 Linux(systemd)
|
|
173
|
-
|
|
174
|
-
两个文件,安装时由 deploy 脚本放置:
|
|
175
|
-
|
|
176
|
-
```
|
|
177
|
-
/etc/systemd/system/w2m-maintenance.path → 监听 queue 目录
|
|
178
|
-
/etc/systemd/system/w2m-maintenance.service → 执行 runner(Type=oneshot)
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
约束:
|
|
182
|
-
|
|
183
|
-
- `User=root`(或专用 `w2m-maint` 用户 + 指定 capability),`NoNewPrivileges=yes` 仍可(不 exec shell)。
|
|
184
|
-
- `ProtectSystem=strict` + `ReadWritePaths=/opt/w2m /var/lib/w2m/maintenance`;`ProtectHome=yes`;`PrivateTmp=yes`;`RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX`;`SystemCallFilter` 白名单按需收紧。
|
|
185
|
-
- runner 只调用内部 JS 模块的固定函数(`update/restart/diag`),**不 exec 用户输入**;health check 用内置 HTTP client。
|
|
186
|
-
- 同一时刻只执行一个请求:`ExecCondition`/flock 串行化;执行期间 path unit 继续触发会被排队(目录里多个文件按 mtime 顺序处理)。
|
|
187
|
-
|
|
188
|
-
### 5.2 Windows(计划任务)
|
|
189
|
-
|
|
190
|
-
- 一个以 `SYSTEM` 运行的计划任务(或 Windows 服务),每分钟轮询 `%ProgramData%\w2m\maintenance\queue`。
|
|
191
|
-
- 同样的 JSON 协议;动作实现用 Node(包内固定模块);服务控制用 SCM API(不调用外部 shell)。
|
|
192
|
-
- 结果写 `done` 目录;agent 读结果回报。
|
|
193
|
-
|
|
194
|
-
### 5.3 目录权限
|
|
195
|
-
|
|
196
|
-
| 路径 | 属主 | 权限 |
|
|
197
|
-
|---|---|---|
|
|
198
|
-
| `queue/` | root | 0770 root:w2m(agent 可写文件,不可删/改他人文件) |
|
|
199
|
-
| `done/` | root | 0750 root:w2m |
|
|
200
|
-
| `state/`(已执行 nonce/request_id) | root | 0700 |
|
|
201
|
-
|
|
202
|
-
agent 写请求时使用 `O_CREAT|O_EXCL`,文件名含 request_id,避免覆盖;结果由 runner 以 root 写。
|
|
203
|
-
|
|
204
|
-
---
|
|
205
|
-
|
|
206
|
-
## 6. 与 relay / agent 的集成(本版本的最小改动)
|
|
207
|
-
|
|
208
|
-
1. **agent**:收到 task 时若存在合法形状的 `maintenance_request` 字段(大小、JSON、协议版本),不走 allow-list,而是:
|
|
209
|
-
- 写入 queue 目录;
|
|
210
|
-
- 轮询 done 目录(≤120s);
|
|
211
|
-
- 把结果文件作为 task 结果回报(status ok/failed,附结果 blob)。
|
|
212
|
-
2. **relay**:透传 `maintenance_request` 字段(限 64 KiB),把 request_id 与 hash 写入 ledger(`maintenance.request` 事件);不解释内容。
|
|
213
|
-
3. **插件工具**(控制端):新增一个调用入口(名字在实现时确定)负责:生成 request → 签名 → 随 task 派发 → 等待结果;**不新增任意命令能力**。
|
|
214
|
-
4. **兼容性**:旧 relay 直接丢弃未知任务字段(不报错)→ 新控制端对旧 relay 返回明确错误 `MAINTENANCE_UNSUPPORTED_RELAY`,不静默失败。
|
|
215
|
-
|
|
216
|
-
> 说明:本版本操作员签名的验证在**目标机 root runner** 内完成;agent 只搬文件。这样即使 agent 被攻破,也无法伪造维护动作。
|
|
217
|
-
|
|
218
|
-
---
|
|
219
|
-
|
|
220
|
-
## 7. 状态机与幂等
|
|
221
|
-
|
|
222
|
-
```
|
|
223
|
-
pending → claimed(runner 读取并加锁) → executing → done | failed | rolled_back
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
- `request_id` 幂等:done 目录已存在同名结果 → 直接返回既有结果,不重复执行。
|
|
227
|
-
- 重放:nonce + request_id 同时进入本地已执行集合(`state/`,跨重启保留),重复请求返回 `MAINTENANCE_REPLAY`。
|
|
228
|
-
- 超时:executing 超过 15 分钟由 runner 自终止并写 `failed`(`MAINTENANCE_TIMEOUT`)。
|
|
229
|
-
- 崩溃恢复:启动时扫描 `claimed/` 下的残留文件,转为 `failed` 并记录。
|
|
230
|
-
|
|
231
|
-
---
|
|
232
|
-
|
|
233
|
-
## 8. 验收(本版本必须用真实任务触发)
|
|
234
|
-
|
|
235
|
-
### 8.1 功能验收(真实机器)
|
|
236
|
-
|
|
237
|
-
1. **空 allow-list 机器升级**:在一台 allow-list 为空的机器(验收对象:TwinsEarth / jp-shared)上,操作员签一次 `update` 请求 → 目标机完成插件升级 → 重启 → 版本回报 `1.3.2-beta.x`。全程不需要 SSH。
|
|
238
|
-
2. **relay 升级**:同样流程升级 relay 主机(验收对象:jp-shared),relay_id 变化、uptime 归零、`/healthz` 200、任务状态从持久化恢复。
|
|
239
|
-
3. **重启**:对 agent/relay 分别发 `restart`,验证固定顺序与健康等待。
|
|
240
|
-
4. **诊断**:发 `diag`,结果包含版本/服务/日志/磁盘,并作为 blob 可读取。
|
|
241
|
-
5. **审计**:ledger 中有 `maintenance.request`(request_id、hash、机器、结果状态),与机器本地日志一致。
|
|
242
|
-
|
|
243
|
-
### 8.2 负向验收(必须自动化,进单测/集成测试)
|
|
244
|
-
|
|
245
|
-
```
|
|
246
|
-
· 未签名 / 签名错误 / 未知 key_id → 拒绝
|
|
247
|
-
· 过期 / 未来 issued_at / TTL>10min → 拒绝
|
|
248
|
-
· machine_id 不匹配 → 拒绝
|
|
249
|
-
· 重放同一 request_id / nonce → 拒绝
|
|
250
|
-
· action 不在白名单 → 拒绝
|
|
251
|
-
· update: sha 不符 / 包内版本不符 / URL 非 https → 拒绝,不落盘执行
|
|
252
|
-
· restart: 目标越界(如 sshd)→ 拒绝
|
|
253
|
-
· diag: 输出超限 → 截断并标记
|
|
254
|
-
· relay 透传被篡改 params → 签名不符 → 拒绝
|
|
255
|
-
· agent 伪造 request 文件(无签名)→ 拒绝
|
|
256
|
-
· 执行失败 → 回滚成功且结果标记 rolled_back
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
### 8.3 兼容与回归
|
|
260
|
-
|
|
261
|
-
- 旧版 agent + 新版 relay:不崩溃,返回 `MAINTENANCE_UNSUPPORTED_AGENT`。
|
|
262
|
-
- 新版 agent + 旧版 relay:任务里无字段,行为与 v1.3.1 一致。
|
|
263
|
-
- 无 DSH / 无 relay 的纯本地路径:手工把请求文件放入 queue 也能执行(验收脚本模拟)。
|
|
264
|
-
|
|
265
|
-
---
|
|
266
|
-
|
|
267
|
-
## 9. 失败模式与回滚
|
|
268
|
-
|
|
269
|
-
| 失败 | 处置 |
|
|
270
|
-
|---|---|
|
|
271
|
-
| 下载失败/超时 | 重试下一个 URL(≤3),全失败 → failed,不改动现有安装 |
|
|
272
|
-
| sha 不符 | 删除临时文件,failed(不换树) |
|
|
273
|
-
| 换树后健康检查失败 | 自动回滚到 app.old + 重启,结果 `rolled_back` |
|
|
274
|
-
| 重启后 agent 不心跳 | relay 侧任务超时;控制端可再发一次 restart(幂等) |
|
|
275
|
-
| 请求文件损坏 | runner 拒绝并写 failed,不执行 |
|
|
276
|
-
| 队列目录被清空 | 本地 `state/` 仍能防重放;结果目录丢失只影响审计展示,不影响执行 |
|
|
277
|
-
| 操作员私钥泄露 | 轮换 operator key;旧 key_id 进撤销列表(request 校验时拒绝) |
|
|
278
|
-
|
|
279
|
-
---
|
|
280
|
-
|
|
281
|
-
## 10. 开放问题(实现前需要确认)
|
|
282
|
-
|
|
283
|
-
1. **操作员签名密钥的形态**:复用现有 operator token 派生 Ed25519?还是引入独立的 operator key(推荐,职责分离)?密钥存 DSH 配置还是系统 keychain?
|
|
284
|
-
2. **Windows 优先级**:1.3.2-beta 是否先只做 Linux(TwinsEarth 是 Windows,需要确认它是否必须在本版本覆盖)?若必须,Windows runner 是否用计划任务(SYSTEM)即可。
|
|
285
|
-
3. **macOS**:1.3.3+ 再做,还是 1.3.2 一并设计(实现可延后)?
|
|
286
|
-
4. **升级目标范围**:本版本只升级"本插件包",还是也支持 relay 包(同一 tarball)与 DSH 本体?建议先只做本插件 + relay(同一包),DSH 本体单列。
|
|
287
|
-
5. **健康检查的判定**:relay `/healthz` 200 是否足够?是否要加"任务能派发成功"的端到端探针(更真实但更重)。
|
|
288
|
-
|
|
289
|
-
---
|
|
290
|
-
|
|
291
|
-
## 11. 交付物清单(实现阶段)
|
|
292
|
-
|
|
293
|
-
```
|
|
294
|
-
src/maintenance/protocol.mjs request 形状/规范化/签名与验证(与 M2 共用)
|
|
295
|
-
src/maintenance/actions/update.mjs
|
|
296
|
-
src/maintenance/actions/restart.mjs
|
|
297
|
-
src/maintenance/actions/diag.mjs
|
|
298
|
-
src/maintenance/runner.mjs root 侧入口,固定 action 分派 + 状态机
|
|
299
|
-
src/agent/agent.mjs 新增 maintenance_request 搬运与结果回报(小改动)
|
|
300
|
-
src/relay/state.mjs + server.mjs 字段透传 + ledger 事件(小改动)
|
|
301
|
-
src/plugin/tools.mjs 控制端发起入口(签名→派发→等待)
|
|
302
|
-
deploy/server/w2m-maintenance.path / .service(Linux)
|
|
303
|
-
deploy/windows/w2m-maintenance.xml(Windows 计划任务)
|
|
304
|
-
test/maintenance-protocol.test.mjs / test/maintenance-runner.test.mjs
|
|
305
|
-
test/maintenance-e2e.test.mjs(本地模拟 relay + 真 root 不可行时的降级方案)
|
|
306
|
-
docs/COLLAB/M1-MAINTENANCE.md(本文)
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
## 12. 版本与发布
|
|
310
|
-
|
|
311
|
-
- `1.3.2-beta.1` 起;beta 政策:GitHub prerelease,不入 npm。
|
|
312
|
-
- 门禁:preflight 两遍 GREEN + M1 验收任务实跑(至少 Linux 一台 + 可选 Windows 一台)。
|
|
313
|
-
- 向下兼容:不改变现有 task/agent 协议,只新增可选字段与新文件;旧机器不受影响。
|
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
# M2 任务级签名授权(grant)设计规格
|
|
2
|
-
|
|
3
|
-
> 状态:**设计稿**。只定义协议、边界、失败语义与验收,不含实现。
|
|
4
|
-
> 版本:`1.3.3-beta.*`(GitHub prerelease,不入 npm)。
|
|
5
|
-
> 上游:`docs/MULTI-MACHINE-COLLABORATION-PLAN.md` M2;与 `docs/COLLAB/M1-MAINTENANCE.md` 共用签名/规范化模块。
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## 1. 目标与非目标
|
|
10
|
-
|
|
11
|
-
### 目标
|
|
12
|
-
|
|
13
|
-
1. **单次授权**:操作员对"某台机器上执行某条具体 argv"签一次名,agent 验签后放行一次;无需把该命令写进永久 allow-list。
|
|
14
|
-
2. **可审计**:每次使用 grant 的任务在 relay ledger 中留下 `grant_id / argv_hash / machine / 结果`,能回答"这条命令是谁授权的、什么时候、在哪些机器上"。
|
|
15
|
-
3. **可撤销**:停止签发新 grant;对被撤销的 grant_id,agent 从 relay 的撤销集拒绝执行(在 TTL 窗口内也拒绝)。
|
|
16
|
-
4. **与 allow-list 共存**:allow-list 仍是默认路径(只读/常规命令);grant 是**显式例外通道**,不是替代品。
|
|
17
|
-
5. **不放宽红线**:仍然只接受 argv 数组(不接受 shell 字符串)、仍受 cwd 限制、超时、输出上限、写范围约束。
|
|
18
|
-
|
|
19
|
-
### 非目标(本版本不做)
|
|
20
|
-
|
|
21
|
-
- 不做"任意命令的长期通行证"(grant 必须绑定 argv_hash + TTL + 机器集合)。
|
|
22
|
-
- 不做交互式会话(不是 SSH 替代品)。
|
|
23
|
-
- 不做提权(agent 仍以自身用户执行;root 动作只走 M1 的固定 action)。
|
|
24
|
-
- 不做"操作员密钥硬件保护"(HSM/智能卡属后续)。
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## 2. 授权分层(本版本落地 L2,L1 沿用)
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
L0 allow-list(机器本地静态配置,v1.3.1 已有)
|
|
32
|
-
L1 签名维护请求(M1:固定 action,root 单元执行;不走 allow-list)
|
|
33
|
-
L2 任务级 grant(本版本:任意 argv 的单次授权;agent 侧验签)
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
判定顺序(agent 收到 offer 时):
|
|
37
|
-
|
|
38
|
-
```
|
|
39
|
-
1. 任务是否带 grant?
|
|
40
|
-
2. 有 grant → 验证(见 §5)→ 通过则**跳过 allow-list**,进入执行前检查(cwd/写范围/超时)
|
|
41
|
-
拒绝则按 grant 错误码返回;不回落 allow-list(防止"签错名但恰好命令在清单里"的歧义)
|
|
42
|
-
3. 无 grant → 走 allow-list(行为与 v1.3.1 完全相同)
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
边界规则:
|
|
46
|
-
|
|
47
|
-
| 场景 | 结果 |
|
|
48
|
-
|---|---|
|
|
49
|
-
| allow-list 命中的普通命令 | 照旧执行,ledger 记录 `auth: "allow-list"` |
|
|
50
|
-
| grant 命中的命令(即使也在 allow-list 里) | 执行,ledger 记录 `auth: "grant"` + grant_id(审计优先显示显式授权) |
|
|
51
|
-
| 两者都不命中 | `COMMAND_NOT_ALLOWED`(与现在一致) |
|
|
52
|
-
| grant 无效 | 对应 `GRANT_*` 错误码,不执行 |
|
|
53
|
-
|
|
54
|
-
---
|
|
55
|
-
|
|
56
|
-
## 3. Grant 对象(签名载荷)
|
|
57
|
-
|
|
58
|
-
UTF-8 JSON,**≤ 8 KiB**,字段如下(签名覆盖除 `signature` 外全部):
|
|
59
|
-
|
|
60
|
-
```json
|
|
61
|
-
{
|
|
62
|
-
"protocol": "w2m-grant/v1",
|
|
63
|
-
"grant_id": "01M6...26 字符 ULID",
|
|
64
|
-
"task_id": "被授权任务的 id(可空;非空时必须与 offer 的 task_id 相等)",
|
|
65
|
-
"machine_ids": ["1c14d7d2-…", "6c92b251-…"],
|
|
66
|
-
"argv_hash": "sha256(canonical argv 数组)",
|
|
67
|
-
"argv_head": ["node", "-e", "console.log('hi')"],
|
|
68
|
-
"max_uses": 1,
|
|
69
|
-
"issued_at": "2026-10-21T13:00:00Z",
|
|
70
|
-
"expires_at": "2026-10-21T13:10:00Z",
|
|
71
|
-
"nonce": "16–64 字符随机串",
|
|
72
|
-
"issued_by": "operator 名(仅审计)",
|
|
73
|
-
"signature": { "alg": "ed25519", "key_id": "operator 公钥指纹前 16 hex", "value_base64": "…" }
|
|
74
|
-
}
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
规范化:键按字典序、无空白、UTF-8;`argv_hash = sha256(JSON.stringify(argv))`,argv 为字符串数组原样(不做 shell 解析、不做路径展开)。
|
|
78
|
-
|
|
79
|
-
### 3.1 约束字段(同一 grant 内可再收紧执行面)
|
|
80
|
-
|
|
81
|
-
```json
|
|
82
|
-
"mode": "replicate|broadcast", // 可选;与 task 不一致则拒绝
|
|
83
|
-
"cwd_rel": ".", // 可选;与 task 不一致则拒绝
|
|
84
|
-
"write": false, // 可选;grant 不能把 write 从 false 放宽到 true
|
|
85
|
-
"write_scope": ["src/"] // 可选;只能收窄,不能扩大机器配置
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
原则:**grant 只能收紧,不能放大机器自身的执行约束**(写范围、cwd、超时、输出上限都由机器说了算;grant 只能进一步限制)。
|
|
89
|
-
|
|
90
|
-
---
|
|
91
|
-
|
|
92
|
-
## 4. Grant 的签发与下发
|
|
93
|
-
|
|
94
|
-
### 4.1 签发(控制端插件内)
|
|
95
|
-
|
|
96
|
-
- 操作员密钥:独立 Ed25519(与 M1 共用同一 operator key;**不复用** operator token 明文)。
|
|
97
|
-
- 输入:argv、目标 machine_ids、TTL(≤10 分钟)、可选 task_id/max_uses。
|
|
98
|
-
- 输出:grant JSON + 签名;控制端把 grant 随任务派发(POST /v1/task 的 `grant` 字段)。
|
|
99
|
-
- 操作员看到的是**展开的 argv 列表**(`argv_head`),签名覆盖 hash;UI/CLI 必须展示完整 argv 后才允许签名(防止"签 hash 但不知道内容")。
|
|
100
|
-
|
|
101
|
-
### 4.2 relay 行为
|
|
102
|
-
|
|
103
|
-
1. POST /v1/task 接收 `grant`(≤8 KiB):
|
|
104
|
-
- 校验形状与协议版本(不验签,验签权威在 agent);
|
|
105
|
-
- 拒绝 `machine_ids` 之外的 lease(只给列出的机器 offer);
|
|
106
|
-
- 校验 `expires_at` 未过期(早拒);过期拒绝 `GRANT_EXPIRED`。
|
|
107
|
-
2. ledger 写 `task.created` 时附 `grant: {grant_id, argv_hash, key_id, max_uses, expires_at}`(**不存完整签名**,避免膨胀;完整 grant 随 offer 下发)。
|
|
108
|
-
3. offer 携带 `grant`;agent 验签。
|
|
109
|
-
4. GET task 的机器结果里暴露 `grant_status`(verified/refused + 错误码)供审计。
|
|
110
|
-
5. 撤销集:relay 内存 + ledger 记录 `grant.revoked` 事件;GET /v1/fleet/grants/revoked 供 agent 拉取(缓存 30s)。
|
|
111
|
-
|
|
112
|
-
### 4.3 agent 验签与使用
|
|
113
|
-
|
|
114
|
-
```
|
|
115
|
-
1. 形状/大小/协议版本
|
|
116
|
-
2. 找 operator 公钥:本地 pinned key(优先)→ relay 下发(TOFU,见 §6)
|
|
117
|
-
3. 验签(canonical 重构)
|
|
118
|
-
4. 时间窗口:issued_at ≤ now ≤ expires_at(时钟偏差容忍 ±60s)
|
|
119
|
-
5. machine_ids 包含本机
|
|
120
|
-
6. task_id 若存在必须等于 offer.task_id
|
|
121
|
-
7. argv_hash 等于 sha256(canonical argv)(逐字节比较)
|
|
122
|
-
8. max_uses:本地持久化使用计数;单机视角计数(同一 grant 给 N 台机器时每台最多 max_uses 次)
|
|
123
|
-
9. nonce 未在本地见过(防重放,跨重启保留)
|
|
124
|
-
10. 撤销检查(relay 撤销集缓存命中则拒绝)
|
|
125
|
-
11. mode/cwd_rel/write/write_scope 与任务实际值一致或更严格
|
|
126
|
-
12. 通过 → 执行;执行结果附 grant_status:"verified"、grant_id、used 次数
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
---
|
|
130
|
-
|
|
131
|
-
## 5. 失败码(全部为任务级 refusal,不抛异常)
|
|
132
|
-
|
|
133
|
-
```
|
|
134
|
-
GRANT_MISSING 任务要求 grant 但没带(仅当任务显式声明 require_grant)
|
|
135
|
-
GRANT_UNSUPPORTED_AGENT 旧 agent 不认识 grant(relay 侧探测后提示)
|
|
136
|
-
GRANT_BAD_SIGNATURE 签名不符
|
|
137
|
-
GRANT_UNKNOWN_KEY key_id 找不到公钥
|
|
138
|
-
GRANT_EXPIRED 过期 / 未到 issued_at / TTL>10min
|
|
139
|
-
GRANT_WRONG_MACHINE machine_ids 不含本机
|
|
140
|
-
GRANT_WRONG_TASK task_id 与 offer 不一致
|
|
141
|
-
GRANT_ARGV_MISMATCH argv_hash 不符
|
|
142
|
-
GRANT_REPLAY nonce/ grant_id 已使用(超 max_uses)
|
|
143
|
-
GRANT_REVOKED 撤销集命中
|
|
144
|
-
GRANT_CONSTRAINT_VIOLATION mode/cwd/write 与任务不符
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
每个失败码都要有自动化负向测试(§8)。
|
|
148
|
-
|
|
149
|
-
---
|
|
150
|
-
|
|
151
|
-
## 6. 密钥分发与信任模型(本版本的关键设计点)
|
|
152
|
-
|
|
153
|
-
**问题**:agent 要验签,就需要操作员公钥。从哪里来?
|
|
154
|
-
|
|
155
|
-
**本版本(1.3.3-beta)方案:TOFU + 轮换链**
|
|
156
|
-
|
|
157
|
-
1. relay 维护 operator 公钥登记:操作员用 operator token(已有的持有证明)调用登记接口,登记 `{key_id, public_key, issued_at, rotation_of?}`;relay 记录 ledger。
|
|
158
|
-
2. agent 首次需要验签时向 relay 拉取公钥(`GET /v1/fleet/operators/:key_id`),**首次接受即固定(TOFU)** 存本地 state。
|
|
159
|
-
3. 之后任何公钥变更必须由**当前 pinned key 签名**(轮换链:`rotation_of = 旧 key_id`,签名覆盖新公钥),agent 验旧签后更新 pinned key;链断则拒绝并告警。
|
|
160
|
-
4. 撤销的公钥进入撤销集,agent 同步后拒绝。
|
|
161
|
-
|
|
162
|
-
**信任声明(写进文档与告警)**:TOFU 阶段,**被攻破的 relay 可以在 agent 首次验签时注入伪造公钥**。这是 1.3.3-beta 的已知限制;缓解与硬化:
|
|
163
|
-
|
|
164
|
-
- 短时间内:配对响应里携带 operator 公钥(pairing 时操作员在场,agent 固定),已有机器通过一条"bootstrap"指令完成 pin;
|
|
165
|
-
- 后续版本(M2.1):强制 pairing-time pinning + 轮换链校验 + relay 侧公钥透明日志(append-only,agent 可校验历史)。
|
|
166
|
-
|
|
167
|
-
---
|
|
168
|
-
|
|
169
|
-
## 7. 与 M1 的关系
|
|
170
|
-
|
|
171
|
-
- 共用:`src/maintenance/protocol.mjs` 的规范化 + Ed25519 验签(M1 放在 root runner,M2 放在 agent)。
|
|
172
|
-
- 差异:M1 的 request 是**固定 action**(update/restart/diag,root 权限);M2 的 grant 是**任意 argv**(agent 权限)。两者都用 operator key 签名,都不扩 allow-list。
|
|
173
|
-
- 组合场景:远程升级 = M1 的 update(root runner);远程"运行升级脚本并检查" = M2 的 grant(agent 权限)。两者不互相替代。
|
|
174
|
-
|
|
175
|
-
---
|
|
176
|
-
|
|
177
|
-
## 8. 验收
|
|
178
|
-
|
|
179
|
-
### 正向(真实任务,必须实跑)
|
|
180
|
-
|
|
181
|
-
1. **空 allow-list 机器执行清单外命令**:TwinsEarth(allow-list 为空)在 grant 授权下执行 `node -e "console.log(process.version)"` 成功,ledger 记录 `auth: "grant"` + grant_id。
|
|
182
|
-
2. **常规命令不受影响**:同一台机器无 grant 的 `npm --version` 仍然 `COMMAND_NOT_ALLOWED`。
|
|
183
|
-
3. **多机 grant**:grant 同时授权 jp-shared + TwinsEarth,两台都成功(各自 max_uses 计数独立)。
|
|
184
|
-
4. **写约束**:grant 带 `write:false` 时写文件的任务被拒绝(`GRANT_CONSTRAINT_VIOLATION` 或写入被拦)。
|
|
185
|
-
5. **撤销**:签发后立即撤销 → 同一 grant 再派发被 `GRANT_REVOKED` 拒绝。
|
|
186
|
-
|
|
187
|
-
### 负向(自动化,进单测/集成)
|
|
188
|
-
|
|
189
|
-
```
|
|
190
|
-
未签名/错签/未知 key_id/过期/未来时间/TTL>10min/换机/换 task_id/改 argv/重放/
|
|
191
|
-
超 max_uses/撤销/约束冲突/大小超限/旧 agent 不认识 grant
|
|
192
|
-
→ 全部对应错误码,任务不执行,ledger 留痕
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
### 审计验收
|
|
196
|
-
|
|
197
|
-
- ledger 中每个 grant 任务:`grant_id / key_id / argv_hash / machine / outcome / grant_status` 可查。
|
|
198
|
-
- 控制端 `w2m_history` 能看到该任务属于 grant 授权(新字段),不需要翻签名原文。
|
|
199
|
-
|
|
200
|
-
---
|
|
201
|
-
|
|
202
|
-
## 9. 兼容与发布
|
|
203
|
-
|
|
204
|
-
- `1.3.3-beta.1` 起;beta 政策:GitHub prerelease,不入 npm。
|
|
205
|
-
- 旧 relay:忽略 `grant` 字段 → 任务按 allow-list 走(可能被拒);控制端检测到旧 relay 返回 `MAINTENANCE_UNSUPPORTED_RELAY`/`GRANT_UNSUPPORTED_RELAY` 提示。
|
|
206
|
-
- 旧 agent:收到带 grant 的 offer 不认识该字段(按现有校验丢失)→ 走 allow-list 拒绝;relay 侧可先行探测 agent 版本并给出 `GRANT_UNSUPPORTED_AGENT`。
|
|
207
|
-
- 不改变 task/offer 现有字段语义,只新增可选字段(`grant`、结果里的 `grant_status`)。
|
|
208
|
-
|
|
209
|
-
---
|
|
210
|
-
|
|
211
|
-
## 10. 交付物清单(实现阶段)
|
|
212
|
-
|
|
213
|
-
```
|
|
214
|
-
src/auth/operator-key.mjs operator key 生成/登记/轮换链/TOFU pin(与 M1 共用)
|
|
215
|
-
src/auth/grant.mjs grant 结构、canonical、签发、验证、撤销集
|
|
216
|
-
src/agent/agent.mjs 验签与使用计数、撤销缓存、结果字段(核心改动)
|
|
217
|
-
src/relay/state.mjs grant 透传、ledger 事件、撤销集、operator 公钥登记
|
|
218
|
-
src/relay/server.mjs POST /v1/task 的 grant 字段、/v1/fleet/operators、撤销集接口
|
|
219
|
-
src/plugin/tools.mjs 控制端签发入口(展示完整 argv → 签名 → 派发 → 等待)
|
|
220
|
-
test/grant.test.mjs 单元:规范化/签名/失败码矩阵
|
|
221
|
-
test/grant-e2e.test.mjs 双机模拟 relay:验签/使用计数/重放/撤销
|
|
222
|
-
docs/COLLAB/M2-TASK-GRANT.md 本文
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
## 11. 开放问题(实现前确认)
|
|
226
|
-
|
|
227
|
-
1. **operator 公钥的初始信任**:接受 TOFU(relay 可注入,文档声明)还是要求 pairing-time pinning 才能开工(更安全但已有机器需要一个 bootstrap 动作)?
|
|
228
|
-
2. **max_uses 语义**:按机器计一次还是全局一次?多机 grant 是否允许每台一次(推荐)?
|
|
229
|
-
3. **task_id 绑定**:是否允许 grant 不绑 task_id(更灵活,但同 argv 可被重复用于不同任务)?建议 beta 强制绑定。
|
|
230
|
-
4. **写授权**:1.3.3-beta 是否允许 `write:true` 的 grant(进入工作区写文件)?还是本版本只做只读命令,写留给 M3 工作区模型?
|
|
231
|
-
5. **Windows**:TwinsEarth 的 agent 在 Windows 上,验签与计数落盘路径需确认;是否与 Linux 同批验收?
|