@twinsearth/w2m-dsh-plugin 2.0.4 → 2.0.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.
Files changed (70) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +1795 -1779
  4. package/bin/w2m-joint.mjs +69 -7
  5. package/codex/mcp-servers.toml +1 -1
  6. package/docs/acceptance/2.0.5/RELEASE-205.json +21 -0
  7. package/docs/acceptance/2.0.5/RELEASE-2050.json +22 -0
  8. package/docs/acceptance/2.0.6/RELEASE-2060.json +22 -0
  9. package/docs/adr/0001-collaboration-crdt.md +59 -0
  10. package/lib/client.js +215 -19
  11. package/package.json +2 -2
  12. package/plugin.json +1 -1
  13. package/src/agent/agent.mjs +97 -36
  14. package/src/agent/artifact-delivery.mjs +89 -0
  15. package/src/agent/caps.mjs +16 -2
  16. package/src/agent/sinks/git-sink.mjs +78 -0
  17. package/src/agent/sinks/ssh-sink.mjs +83 -0
  18. package/src/agent/sse-cursor.mjs +16 -0
  19. package/src/agent/sse-watchdog.mjs +71 -0
  20. package/src/plugin/dataplane-sink-route.mjs +118 -0
  21. package/src/plugin/fleet-chat-route.mjs +11 -0
  22. package/src/plugin/joint-dev/dispatch.mjs +5 -1
  23. package/src/plugin/joint-dev/merge-approval.mjs +115 -0
  24. package/src/plugin/joint-dev/progress.mjs +107 -0
  25. package/src/plugin/joint-dev/relay-dispatch.mjs +52 -22
  26. package/src/plugin/project-stream-route.mjs +58 -0
  27. package/src/plugin/tools-joint.mjs +6 -1
  28. package/src/plugin/tools.mjs +25 -5
  29. package/src/relay/fleet-messages.mjs +48 -24
  30. package/src/relay/project-room.mjs +47 -2
  31. package/src/relay/server.mjs +11 -5
  32. package/src/relay/state.mjs +63 -3
  33. package/src/util/chat-crypto.mjs +70 -1
  34. package/docs/COLLAB/COLLABORATION-MODEL.md +0 -240
  35. package/docs/COLLAB/EXECUTION-AUTH-MODEL.md +0 -361
  36. package/docs/COLLAB/M1-ACCEPTANCE-RUNBOOK.md +0 -70
  37. package/docs/COLLAB/M1-MAINTENANCE.md +0 -313
  38. package/docs/COLLAB/M2-TASK-GRANT.md +0 -231
  39. package/docs/COLLAB/M3-GIT-NATIVE-TASKS.md +0 -291
  40. package/docs/COLLAB/M4-PROJECT-ROOM.md +0 -260
  41. package/docs/COLLAB/M5-MULTI-CLIENT-PRIMITIVES.md +0 -263
  42. package/docs/COLLAB/M6-CROSS-NETWORK-RELIABILITY.md +0 -231
  43. package/docs/COLLAB/M7-METRICS-ACCEPTANCE.md +0 -263
  44. package/docs/COLLAB/TASK-MODEL.md +0 -281
  45. package/docs/acceptance/1.9.10/RELEASE-1910.json +0 -21
  46. package/docs/acceptance/1.9.11/RELEASE-1911.json +0 -20
  47. package/docs/acceptance/1.9.12/RELEASE-1912.json +0 -20
  48. package/docs/acceptance/1.9.13/RELEASE-1913.json +0 -20
  49. package/docs/acceptance/1.9.14/RELEASE-1914.json +0 -20
  50. package/docs/acceptance/1.9.15/RELEASE-1915.json +0 -22
  51. package/docs/acceptance/1.9.16/RELEASE-1916.json +0 -21
  52. package/docs/acceptance/1.9.17/RELEASE-1917.json +0 -20
  53. package/docs/acceptance/1.9.19/RELEASE-1919.json +0 -21
  54. package/docs/acceptance/1.9.20/RELEASE-1920.json +0 -22
  55. package/docs/acceptance/1.9.4/RELEASE-194.json +0 -23
  56. package/docs/acceptance/1.9.5/RELEASE-195.json +0 -23
  57. package/docs/acceptance/1.9.6/RELEASE-196.json +0 -22
  58. package/docs/acceptance/1.9.7/RELEASE-197.json +0 -21
  59. package/docs/acceptance/1.9.8/RELEASE-198.json +0 -21
  60. package/docs/acceptance/1.9.9/RELEASE-199.json +0 -21
  61. package/lib/client.js.bak2 +0 -640
  62. package/src/plugin/dataflow.mjs +0 -72
  63. package/src/plugin/fleet-target.mjs.bak-arch2 +0 -812
  64. package/src/plugin/fleet-target.mjs.bak-archive +0 -812
  65. package/src/plugin/tofuture/report.mjs +0 -44
  66. package/src/plugin/tools.mjs.bak-c7 +0 -7029
  67. package/src/relay/tofuture-whitelist.mjs +0 -46
  68. package/src/util/data-plane.mjs +0 -180
  69. package/src/util/sealed-object.mjs +0 -141
  70. package/src/util/tofuture-hmac.mjs +0 -30
@@ -1,240 +0,0 @@
1
- # 协作模型(Collaboration Model)设计规格 — v1.4.2-beta
2
-
3
- > 状态:**模型级设计稿**。把 M4(项目房间)与 M5(多客户端原语)归纳为统一协作模型:共享对象、成员与角色、事件一致性、多客户端视图。
4
- > 版本:`1.4.2-beta.*`;实现按 M4(1.3.5)与 M5(1.3.6)分步交付,模型一致性验收在 1.4.2-beta 完成。
5
- > 相关:`docs/COLLAB/M4-PROJECT-ROOM.md`、`docs/COLLAB/M5-MULTI-CLIENT-PRIMITIVES.md`、`docs/COLLAB/TASK-MODEL.md`。
6
-
7
- ---
8
-
9
- ## 1. 问题陈述
10
-
11
- 现状的"协作"只有三件互不关联的东西:
12
-
13
- ```
14
- · relay 的派发/聚合(任务属于调用者,不属于任何共享对象)
15
- · 各 DSH 实例自己的会话(多客户端各看各的)
16
- · fleet chat 消息流(是消息,不是共享状态)
17
- ```
18
-
19
- 缺的是**共享对象**:project / room / board / artifact / decision。没有它们:
20
-
21
- - 多客户端无法看到同一份任务板与制品;
22
- - 权限、成员、角色无处附着;
23
- - 通知没有"项目"这个投递范围;
24
- - 内存/决策是全局的,多项目互相污染。
25
-
26
- ---
27
-
28
- ## 2. 模型总览
29
-
30
- ```
31
- 协作 = 主体 × 项目 × 对象 × 关系 × 交互 × 事件
32
-
33
- 主体 machine(设备身份) / operator(操作员密钥身份)
34
- 项目 project(聚合根):成员、角色、设置、origin
35
- 对象 任务板(board)、制品(artifact)、分支 refs、memory 文档、决策(decision)、
36
- 审查(review)、锁(lock)、通知(notification)
37
- 关系 成员资格 + 角色(owner/maintainer/worker/observer)
38
- 交互 派发任务、提交/裁决审查、获取/释放锁、读写通知、读板/制品
39
- 事件 追加式事件流(seq 单调),快照由事件投影而来;客户端只是视图
40
- ```
41
-
42
- **单一事实源**:项目事件流(+ ledger 长期审计)。客户端不做本地权威状态,只保存视图与水位(已应用 seq、已读通知水位)。
43
-
44
- ---
45
-
46
- ## 3. 核心概念与不变量
47
-
48
- ### 3.1 项目(聚合根)
49
-
50
- - 一个 project 有唯一 `project_id`、`origin`(git 或无)、`settings`、`members`。
51
- - 任务、制品、refs、memory、decision、review、lock、notification 都必须挂在 project 下(旧任务惰性归入 default 项目)。
52
- - 项目隔离:非成员不能读项目快照/事件/制品(错误信息不泄露存在性)。
53
-
54
- ### 3.2 成员与角色
55
-
56
- | 角色 | 可以做什么 |
57
- |---|---|
58
- | observer | 只读(板/制品/决策/refs/通知) |
59
- | worker | + 派发任务、登记制品、申请/持有锁 |
60
- | maintainer | + 写 memory/决策、审查裁决、项目设置 |
61
- | owner | + 管理成员、创建项目、强制释放锁 |
62
-
63
- 权限判定统一为 `mayAct(role, action)`(拒绝默认,返回 `required_role/current_role`)。角色变更本身是事件(`member.role_changed`),可审计。
64
-
65
- ### 3.3 共享对象
66
-
67
- | 对象 | 事实源 | 视图 |
68
- |---|---|---|
69
- | 任务板 | 任务状态事件(M3 任务模型) | 四列(waiting/active/done/failed)+ 计数 |
70
- | 制品 | blob(内容寻址)+ 索引 | 列表/sha 下载 |
71
- | refs | 合并队列推进(M3) | 分支 head |
72
- | memory | CAS 文档(项目命名空间) | 索引 + 最近记录 |
73
- | 决策 | 审查/council 决策事件 | 决策列表(含异议) |
74
- | 审查 | review 对象(M5) | 待审/已审 |
75
- | 锁 | lock 对象(M5) | 持有人/TTL |
76
- | 通知 | 事件流派生 | 未读 + 列表 |
77
-
78
- ### 3.4 事件与一致性
79
-
80
- ```
81
- · 每个项目一条追加式事件流,seq 单调(relay 权威排序)
82
- · 快照 = 事件流在某个 seq 的投影,快照携带 last_event_seq(一致性点)
83
- · 客户端增量:只应用 seq > 本地最大;发现缺口 → 重拉快照(绝不静默跳过)
84
- · 客户端写后读:自己的写请求返回事件 seq;后续读至少看到该 seq(read-your-writes)
85
- · 其他客户端:最终一致(通常 < 3s,实测目标)
86
- · 消息/聊天不进事实源;聊天是旁路通道,不产生共享状态
87
- ```
88
-
89
- ### 3.5 存在性(presence)与水位
90
-
91
- - presence = 设备在线状态 + 最后心跳(复用 M0 的设备表),用于显示"谁在线"。
92
- - 每个客户端维护两类水位:`applied_seq`(视图一致性)与 `read_watermark_ms`(通知已读)。
93
- - 水位持久化在客户端;服务端不追踪"某人读到哪"(隐私与规模考虑),只用未读计数接口。
94
-
95
- ---
96
-
97
- ## 4. 交互协议(模型级)
98
-
99
- | 交互 | 入口 | 语义 |
100
- |---|---|---|
101
- | 选择/创建项目 | 项目 API + 面板 | 显式创建或选择 default |
102
- | 派发任务 | task API + `project_id` | 任务归属项目,进板 |
103
- | 提交审查 | review API | 指向 patch 制品,指定审查者 |
104
- | 审查裁决 | review decision API | approve / changes_requested + findings |
105
- | 获取/释放锁 | lock API | 分支/文件,TTL,强制释放(owner) |
106
- | 读板/制品/决策/refs | 快照 + 增量 | 只读视图 |
107
- | 通知 | 事件流 + unread + read | 实时 SSE + 离线补齐 |
108
- | 订阅 | SSE `since_seq` | 断线重连续传 |
109
-
110
- 所有写操作都要求成员资格 + 角色权限;所有写操作产生事件;所有事件可重放。
111
-
112
- ---
113
-
114
- ## 5. 多客户端的典型场景
115
-
116
- ### 场景 A:两人联合修一个 bug
117
-
118
- ```
119
- A(机器 1)打开 project → 派发 worktree 任务 → 产出 patch 制品
120
- A 提交审查(指定 B)→ B(机器 2)收到通知 → 拉 patch → 批准
121
- 合并队列执行 → ref.updated → 双方板/refs/通知同步更新
122
- ```
123
-
124
- ### 场景 B:离线后补齐
125
-
126
- ```
127
- B 离线 2 分钟 → 重连:applied_seq 落后 → 拉增量事件补齐(或超窗口则重拉快照)
128
- unread 通知计数 → 标记已读 → 归零;期间任务状态变化全部可见
129
- ```
130
-
131
- ### 场景 C:并发冲突
132
-
133
- ```
134
- A/B 同时改同一文件:enforced 模式下 A 持文件锁,B 获取失败(LOCK_HELD)
135
- B 等待/改方案;A 释放后 B 继续;最终合并或结构化 MERGE_CONFLICT 由人解决
136
- ```
137
-
138
- ### 场景 D:relay 重启
139
-
140
- ```
141
- 持久化恢复:项目/成员/事件 seq 连续;客户端断线重连后按 seq 补齐;
142
- 任务租约重派发;不丢事件、不重复应用
143
- ```
144
-
145
- ---
146
-
147
- ## 6. 故障与保证
148
-
149
- | 故障 | 保证 |
150
- |---|---|
151
- | 客户端断线 | 重连补齐;缺口→快照;不丢事件 |
152
- | 客户端离线写 | 写请求失败即明确报错(不做本地离线写队列,避免冲突) |
153
- | relay 重启 | 状态持久化 + seq 连续;快照可重建 |
154
- | 成员被移除 | 立即失去读写(事件 `member.removed`);在途任务按任务语义完成或取消 |
155
- | 权限不足 | 结构化拒绝(code + required/current role),不泄露对象是否存在 |
156
- | 通知风暴 | 去重 + 限流 + digest(M5) |
157
- | 事件流损坏 | 跳过并告警;ledger 可重建;快照兜底 |
158
- | 制品被删 | 引用处明确报错 `ARTIFACT_MISSING`,不返回空内容 |
159
-
160
- 投递语义:
161
-
162
- - 事件:at-least-once 应用(客户端按 seq 幂等);
163
- - 通知:at-least-once + 去重 + 水位;
164
- - 审查结论:不可变,重复同向结论拒绝;
165
- - 锁:TTL 过期自动释放,强占需 owner 且留痕。
166
-
167
- ---
168
-
169
- ## 7. 安全与隐私
170
-
171
- 1. **身份分离**:machine(设备令牌)与 operator(签名密钥)是两类主体,成员资格显式绑定。
172
- 2. **项目隔离**:跨项目不可见;错误不泄露存在性;制品按成员资格授权。
173
- 3. **最小权限**:角色矩阵(§3.2);拒绝默认。
174
- 4. **审计**:成员变更、角色变更、锁强制释放、审查结论、决策全部进事件 + ledger。
175
- 5. **隐私**:客户端水位本地保存;服务端不追踪个人阅读明细(只给未读计数)。
176
- 6. **不把聊天当状态**:消息可被编辑策略删除/分享,因此不进入项目事实源。
177
-
178
- ---
179
-
180
- ## 8. 兼容与迁移
181
-
182
- - 新 API 全部新增;旧客户端/旧 relay 不受影响(旧 relay → `PROJECT_ROOM_UNSUPPORTED_RELAY`)。
183
- - 旧任务:启动时惰性归入 default 项目(不重写 ledger)。
184
- - 旧 fleet memory:保留为 fleet 级;项目决策写 `project:<id>:decisions`。
185
- - fleet chat:保留为旁路通道(人不执行聊天内容;聊天不产生任务)。
186
- - 三个模型的关系:执行授权(能否执行)→ 任务(执行什么)→ 协作(谁/在哪/共享什么)。
187
-
188
- ---
189
-
190
- ## 9. 验收(模型一致性,真实任务)
191
-
192
- ### 正向
193
-
194
- 1. **同一任务板**:A 派发任务 → B(另一台机器)3 秒内看到;状态变化同步。
195
- 2. **制品共享**:A 的 patch 制品登记 → B 可下载且 sha 一致。
196
- 3. **决策共享**:A 记录决策(含异议)→ B 决策列表可见。
197
- 4. **审查门禁联动**:A 提交审查 → B 批准 → 合并门禁放行。
198
- 5. **锁协作**:enforced 模式下 A 持锁 B 被拒;释放后 B 成功;双方收到锁事件。
199
- 6. **离线补齐**:B 断线 2 分钟 → 重连后事件补齐、未读通知计数正确、已读归零。
200
- 7. **重启恢复**:relay 重启后项目/成员/seq 连续,客户端补齐无重复。
201
-
202
- ### 负向(自动化矩阵)
203
-
204
- ```
205
- 非成员读写 → 403/404(不泄露)
206
- observer 派发/登记 → 拒绝
207
- worker 审查裁决 → REVIEW_PERMISSION
208
- 移除成员后访问 → 立即失败
209
- seq 缺口 → 强制快照(不静默)
210
- 重复事件应用 → 幂等
211
- 通知风暴 → digest 且不丢
212
- 锁被盗用/过期操作 → LOCK_*
213
- 制品被删后的引用 → ARTIFACT_MISSING
214
- ```
215
-
216
- ---
217
-
218
- ## 10. 与版本/模块的映射
219
-
220
- ```
221
- 1.3.5-beta M4 实现:project 对象、快照+增量、权限、面板只读视图
222
- 1.3.6-beta M5 实现:锁/审查/通知
223
- 1.4.2-beta 模型一致性验收:统一对象/事件/权限/水位语义,双客户端真实任务证据
224
- 模块:
225
- src/relay/project-room.mjs 项目/成员/角色/设置
226
- src/relay/project-events.mjs 事件流/快照/增量/SSE
227
- src/relay/project-artifacts.mjs 制品索引
228
- src/relay/locks.mjs / reviews.mjs / notifications.mjs(M5)
229
- src/plugin/tools.mjs + fleet-chat-route.mjs + lib/client.js(客户端视图)
230
- ```
231
-
232
- ## 11. 开放问题(实现前确认)
233
-
234
- 1. **身份维度**:本版本用 machine+operator,还是先引入"人"的账号模型(影响成员/通知/审查)?
235
- 2. **项目创建权**:仅 owner,还是 maintainer 也可创建?
236
- 3. **default 项目可见性**:所有成员可见,还是仅 owner(旧任务迁移策略)?
237
- 4. **通知渠道**:只做项目房间内(推荐),还是同时接 fleet chat mentions?
238
- 5. **presence 范围**:只显示在线/离线,还是显示"正在编辑哪个任务/持有哪些锁"?
239
- 6. **任务多项目归属**:beta 单项目(推荐)还是允许一任务挂多个项目?
240
- 7. **事件保留**:5000 条/项目是否够用?超出只靠快照是否可接受?
@@ -1,361 +0,0 @@
1
- # 执行授权模型(Execution Authorization Model)设计规格 — v1.4.0-beta
2
-
3
- > 状态:**已实现(v1.3.12-beta.1)**。把 M1(运维通道)与 M2(任务级 grant)归纳成一个可复用的授权模型,并冻结线协议与失败语义。
4
- > 版本:`1.4.0-beta.*`;实现按 M1(1.3.2)/ M2(1.3.3)分步交付,模型一致性验收在 1.4.0-beta 完成。
5
- > 相关:`docs/COLLAB/M1-MAINTENANCE.md`、`docs/COLLAB/M2-TASK-GRANT.md`。
6
-
7
- ---
8
-
9
- ## 1. 问题陈述
10
-
11
- 现状只有一个执行授权来源:**机器本地静态 allow-list**。它带来三个无法回避的后果:
12
-
13
- 1. **远程运维不可能**:升级/重启/排障命令不在清单里,只能 SSH;清单空白的机器(TwinsEarth 实测)连 `node --version` 都拒绝,编队里"在线但不可用"。
14
- 2. **放宽清单是唯一出路,且不安全**:为了升级一次就把安装命令永久写进清单,等于给任何能调度任务的人一个长期入口。
15
- 3. **没有"谁授权了什么"的记录**:allow-list 是配置文件,不是事件;无法回答"这条命令是谁、在何时、为什么被允许"。
16
-
17
- 模型目标:引入**显式的、可审计的、带时限的执行授权**,与 allow-list 并存,且不改变"只执行 argv、不执行 shell 字符串"的红线。
18
-
19
- ---
20
-
21
- ## 2. 模型总览
22
-
23
- ```
24
- ┌────────────── 签发(控制面,操作员私钥)──────────────┐
25
- │ grant / maintenance request(签名能力令牌) │
26
- └──────────────────────────┬──────────────────────────┘
27
- │ relay 传输(不解释内容)
28
- ▼
29
- 目标机 ┌────────────────── 授权判定 authorize(action, context) ──────────────────┐
30
- │ 1. 硬红线检查(永远拒绝) │
31
- │ 2. 能力校验:签名/主体/范围/时限/次数/重放/撤销 │
32
- │ 3. 与 allow-list 组合:显式授权优先;无授权则看清单;都不中则拒绝 │
33
- └──────────────────────────────┬───────────────────────────────────────┘
34
- ▼
35
- 执行面(二选一,权限不同)
36
- A. agent 用户执行(argv)
37
- B. root runner 执行(固定 action:update/restart/diag)
38
- ```
39
-
40
- 核心概念:
41
-
42
- | 概念 | 含义 |
43
- |---|---|
44
- | **Authority** | 操作员密钥(Ed25519),签名的唯一来源 |
45
- | **Capability** | 签名令牌,表达"允许在哪些机器、执行什么、多久、几次" |
46
- | **Subject** | 目标机器(machine_ids)/ 项目(后续 M3 的合并授权) |
47
- | **Scope** | argv_hash + 执行约束(mode/cwd/write/timeout/env 白名单) |
48
- | **Bounds** | TTL、max_uses、task 绑定、nonce、撤销 |
49
- | **Decision** | 允许 / 拒绝(带统一错误码),拒绝默认、无副作用 |
50
- | **Provenance** | 每次允许都记录来源:`allow-list` / `grant` / `maintenance`,进 ledger |
51
-
52
- ---
53
-
54
- ## 3. 能力类型(本版本冻结两种)
55
-
56
- ### 3.1 能力 A:执行授权(grant)——agent 用户权限
57
-
58
- - 主体:`machine_ids[]`
59
- - 范围:`argv_hash`(sha256 of canonical argv)+ 可收紧的 `mode/cwd_rel/write/write_scope/timeout_ms`
60
- - 边界:TTL ≤ 10 分钟、`max_uses`(按机器计)、可绑 `task_id`
61
- - 执行者:agent(w2m 等非特权用户)
62
- - 典型:空 allow-list 机器上执行 `node -e ...`、`npm --version`、诊断脚本
63
-
64
- ### 3.2 能力 B:维护请求(maintenance)——root/SYSTEM 权限
65
-
66
- - 主体:`machine_id`(单机)
67
- - 范围:固定 action 枚举 `update | restart | diag` 及其**白名单参数**(版本、sha256、URL 列表、目标单元)
68
- - 边界:TTL ≤ 10 分钟、单次
69
- - 执行者:root runner(独立单元,固定实现,解释 JSON 但不执行 shell)
70
- - 典型:远程升级插件/relay、重启服务、收集诊断
71
-
72
- > 两种能力共用同一签名载荷/规范化/密钥,但**权限不同、执行者不同、参数白名单不同**。这是模型的核心分离:让"跑一条命令"和"做一次系统维护"不共用危险通道。
73
-
74
- ### 3.3 未来能力(不在本版本)
75
-
76
- - 项目级能力(合并队列的 merge-slot 授权)——由 M3 定义,沿用本模型(主体=project+branch,范围=merge 操作)。
77
- - 角色委托(owner 签发子 key 给 maintainer)——v1.4.x 后续。
78
-
79
- ---
80
-
81
- ## 4. 授权判定函数(冻结语义)
82
-
83
- ```
84
- authorize(input) -> { allow: boolean, provenance, code?, detail? }
85
-
86
- input = {
87
- capability?: SignedCapability, // grant 或 maintenance,可能缺失
88
- action: { kind: "exec"|"maintenance", argv?, constraints?, maintenance? },
89
- context: { machine_id, task_id?, attempt, now, policy: { allow_list, red_lines, limits } },
90
- state: { used_count, nonce_seen, revoked_set, pinned_operator_keys }
91
- }
92
- ```
93
-
94
- 判定顺序(每一步失败都返回对应错误码,不执行、不落副作用):
95
-
96
- ```
97
- 1. 硬红线(任何来源都不能越过)
98
- 1.1 仅 argv 数组;禁止 shell 字符串/命令拼接
99
- 1.2 cwd 必须落在允许根内(项目目录/工作区),无路径穿越
100
- 1.3 禁止提权(agent 通道不能要求 root;maintenance 只走固定 action)
101
- 1.4 写入必须落在 write_scope 内;只读任务不得写
102
- 1.5 输出/超时/CPU/内存上限由机器策略决定,能力只能收紧
103
- → 违规:RED_LINE_VIOLATION
104
-
105
- 2. 有能力(capability 存在)
106
- 2.1 形状/大小/协议版本
107
- 2.2 验签(pinned operator key;未知 key_id → UNKNOWN_AUTHORITY)
108
- 2.3 主体匹配(machine_ids 含本机;maintenance 的 machine_id 必须相等)
109
- 2.4 范围匹配(argv_hash/参数逐项一致;约束不得比机器策略更宽)
110
- 2.5 时限与次数(issued_at ≤ now ≤ expires_at,TTL 上限;used_count < max_uses;task 绑定)
111
- 2.6 重放(nonce + capability_id 本地持久化)
112
- 2.7 撤销(relay 撤销集缓存窗口内命中)
113
- → 通过:allow,provenance = "grant" | "maintenance"
114
-
115
- 3. 无能力
116
- 3.1 allow-list 命中 → allow,provenance = "allow-list"
117
- 3.2 否则 → deny,code = NOT_AUTHORIZED(对 grant 场景为 GRANT_MISSING)
118
-
119
- 4. 记录
120
- 4.1 allow:ledger 记 provenance + capability_id + argv_hash + 结果
121
- 4.2 deny:ledger 记 code + 摘要(不记全量 argv 敏感内容,按红action策略脱敏)
122
- ```
123
-
124
- 组合与优先级:
125
-
126
- - **拒绝优先**:任一步拒绝即拒绝;能力不能覆盖硬红线。
127
- - **能力优先于清单**(用于审计显式性),但结果相同;两者都允许时记录 `grant`(显式授权),并标记 `also_in_allow_list: true`。
128
- - **清单是"常驻授权"**:其 provenance = `local-config`,不在 ledger 伪造操作员签名;模型上把 allow-list 视为"由机器 owner 预签发的长期能力",便于统一解释。
129
-
130
- ---
131
-
132
- ## 5. 信任建立与密钥生命周期
133
-
134
- ### 5.1 操作员密钥
135
-
136
- - 独立 Ed25519,与 operator token(调度凭据)**分离**:token 泄露不等于授权能力泄露,反之亦然。
137
- - 私钥存放在控制端(DSH 配置/OS keychain);不进入 relay、不进入 agent。
138
- - 公钥登记:控制端用 operator token 证明持有权 → relay 记录 `{key_id, public_key, created_at}`(ledger 事件)。
139
-
140
- ### 5.2 目标机信任(agent / root runner)
141
-
142
- ```
143
- 首选(推荐,M1/M2 终态):pairing-time pinning
144
- pairing 响应携带 operator 公钥 → 机器在配对时固定(操作员在场,这是可信锚点)
145
- 轮换:新公钥必须由旧 pinned key 签名(rotation chain),agent 验链后更新
146
- 撤销:操作员登记 revoke;agent 同步后拒绝旧 key
147
-
148
- beta 过渡(1.3.2/1.3.3 实现期):TOFU
149
- 首次验签时向 relay 拉取公钥并固定;文档明确"首次可被攻破的 relay 注入"
150
- 后续版本改为 pairing-time pinning 为默认,TOFU 仅作显式 fallback
151
- ```
152
-
153
- **已知限制(必须写进发布说明)**:TOFU 窗口内,被攻破的 relay 可注入伪造公钥。缓解:短 TTL、审计告警、尽快升级到 pinned 模式。
154
-
155
- ### 5.3 撤销
156
-
157
- - relay 维护撤销集(capability_id、key_id);ledger 记录 `capability.revoked`。
158
- - agent 缓存 30s;在 TTL 内撤销也生效(最多 30s 延迟)。
159
- - 语义声明:撤销**不会**中断正在执行的动作,只阻止新的执行。
160
-
161
- ---
162
-
163
- ## 6. 威胁模型(模型级)
164
-
165
- | 威胁 | 能否得逞 | 依据/处置 |
166
- |---|---|---|
167
- | 恶意编队成员伪造指令 | ❌ | 无操作员私钥,签名不可伪造 |
168
- | 被攻破的 relay 伪造授权 | ⚠️ TOFU 窗口内可注入公钥(文档声明);pinned 后不可 | 尽快 pinning;短 TTL;审计 |
169
- | 被攻破的 relay 扣留/延迟指令 | ✅(可用性) | 授权不影响正确性;重试/审计可发现 |
170
- | 被攻破的 agent | ❌ 提权 | agent 只能以自身用户执行;root 动作只走固定 maintenance action |
171
- | 重放旧授权 | ❌ | nonce + capability_id 持久化 + task 绑定 + TTL |
172
- | 改 argv 绕范围 | ❌ | argv_hash 校验 |
173
- | 越界写文件 | ❌ | 硬红线 cwd/write_scope |
174
- | 时钟回拨延长 TTL | ⚠️ | 服务端时间权威(relay);本地只做窗口判断;偏差容忍 ±60s |
175
- | 操作员误授权 | ⚠️ | 展示完整 argv、TTL ≤10min、max_uses、撤销;高风险动作需二次确认 |
176
-
177
- ---
178
-
179
- ## 7. 兼容与迁移
180
-
181
- - **旧 relay**:忽略 grant/maintenance 字段 → 行为退回 allow-list;控制端检测后返回 `UNSUPPORTED_RELAY`。
182
- - **旧 agent**:不认识字段 → allow-list 判定(维持 v1.3.1 行为),relay 可探测版本并提示 `UNSUPPORTED_AGENT`。
183
- - **旧 allow-list**:模型上保留为常驻授权,行为不变;不需要迁移配置。
184
- - **新安装**:默认仍是"空 allow-list + 无 pinned key",但配对时可选择 pin 操作员公钥(推荐);空清单机器不再"不可用"——只要操作员签一次能力即可。
185
-
186
- ---
187
-
188
- ## 8. 验收(模型一致性,横跨 M1/M2)
189
-
190
- ### 正向(真实任务证据)
191
-
192
- 1. **空 allow-list + grant**:TwinsEarth 执行 `node -e "console.log(...)"`;ledger provenance=`grant`,记录 capability_id/argv_hash。
193
- 2. **空 allow-list + maintenance**:同机完成 `update`/`restart`/`diag`;provenance=`maintenance`,root runner 日志与 ledger 一致。
194
- 3. **普通 allow-list 命令**:provenance=`allow-list`,行为与 1.3.1 identical(对账对照)。
195
- 4. **撤销窗口**:签发→撤销→派发(≤30s 内)被拒;超过缓存窗口同样被拒。
196
- 5. **轮换链**:旧 key 签新 key → agent 更新 pinned;未签的换 key 被拒。
197
-
198
- ### 负向(模型矩阵,自动化)
199
-
200
- ```
201
- 硬红线:shell 字符串/路径穿越/提权/越界写 → RED_LINE_VIOLATION
202
- 能力:缺签/错签/未知 key/过期/未来/换机/换 argv/重放/超次数/撤销/任务绑定不符 →
203
- NOT_AUTHORIZED / UNKNOWN_AUTHORITY / GRANT_* / MAINTENANCE_*
204
- 审计:拒绝不产生执行副作用;ledger 有 deny 记录
205
- 组合:能力拒绝时不回落 allow-list(防歧义);allow-list 命中但能力被篡改 → 拒绝(显式意图无效)
206
- 时钟:±61s 边界、issued_at 在未来、expires_at 恰好过期
207
- ```
208
-
209
- ### 审计查询
210
-
211
- - 给定 task_id → 能力来源、key_id、argv_hash、结果。
212
- - 给定机器 → 当前 pinned key、最近能力使用、拒绝计数。
213
- - 给定时间窗 → 所有 maintenance 动作与 grant 使用(导出 JSON)。
214
-
215
- ---
216
-
217
- ## 9. 与三个模型的关系
218
-
219
- | 模型 | 版本 | 依赖 | 说明 |
220
- |---|---|---|---|
221
- | 执行授权模型(本文) | v1.4.0-beta | 独立 | 定义"能否执行";M1/M2 是其两种实现 |
222
- | 任务模型 | v1.4.1-beta | 本模型(合并/审查任务需授权) | 定义"执行什么、产出什么(worktree/patch/merge)" |
223
- | 协作模型 | v1.4.2-beta | 本模型 + 任务模型 | 定义"谁和谁、在哪个项目/房间协作" |
224
-
225
- ---
226
-
227
- ## 10. 交付物与版本映射
228
-
229
- ```
230
- 模型文档:docs/COLLAB/EXECUTION-AUTH-MODEL.md(本文)
231
- 实现拆分:
232
- 1.3.2-beta M1 运维通道(能力 B:固定 action + root runner)
233
- 1.3.3-beta M2 任务级 grant(能力 A:argv 授权 + agent 验签)
234
- 1.4.0-beta 模型一致性验收:统一错误码/审计查询/轮换链/撤销窗口/真实任务证据
235
- (不新增功能;把 M1/M2 的行为对齐到本模型)
236
- 共享模块:
237
- src/auth/operator-key.mjs 密钥生成/登记/轮换链/pin/撤销
238
- src/auth/capability.mjs 两种能力的统一载荷、规范化、签发、验证
239
- src/auth/authorize.mjs 判定函数(纯函数,输入输出可单测)
240
- src/auth/audit.mjs 审计查询(ledger 上的投影)
241
- ```
242
-
243
- ## 11. 开放问题(实现前确认)
244
-
245
- 1. **pinned vs TOFU 的默认**:1.3.2/1.3.3 实现期是否直接做 pairing-time pinning(更安全,改动稍大)?
246
- 2. **操作员密钥创建 UX**:`dsh` 命令生成并写入 OS keychain,还是插件面板生成?
247
- 3. **能力签发 UX**:一次一条命令(简单)还是"策略模板"(如"升级到某版本"一键签一组)?
248
- 4. **谁可以签**:仅 owner 操作员,还是允许 maintainer 使用预设策略?
249
- 5. **max TTL**:10 分钟是否合适(运维场景可能更长)?
250
- 6. **审计可见性**:ledger 里的 argv 是否永久保存明文(敏感参数)还是只存 hash + 前缀?
251
-
252
- ---
253
-
254
- ## 签发(v1.3.12-beta.1)
255
-
256
- 执行授权模型之前只有静态 allow-list:机器所有者写下一个长期决定,任何不在表里的命令都没有"就这一次"的途径。现在补上
257
- 了操作员显式授权某一次执行的完整路径。
258
-
259
- ### 1. 生成操作员密钥
260
-
261
- 操作员持有 Ed25519 私钥;机器的 allow-list 保持不动(默认拒绝)。
262
-
263
- ### 2. 先取 task_id,再签发 grant
264
-
265
- grant 绑定 **一个 task、一个 argv、若干 machine_id、一个短 TTL(≤10 分钟)和 max_uses(默认 1)**。所以签发之前 task_id
266
- 必须已经确定(调度方生成后传给 `POST /v1/task` 的 `task_id` 字段)。
267
-
268
- ```bash
269
- node bin/w2m-grant.mjs \
270
- --key ~/.w2m/operator.pem \
271
- --task-id 01M5GRANTCLI0000000000000001 \
272
- --machines 6c92b251-3728-48bc-82a9-56cc40df48f2 \
273
- --argv '["node","--version"]' \
274
- --ttl-ms 120000 > /tmp/grant.json
275
- ```
276
-
277
- stdout 是 grant JSON;stderr 打印 `task_id / argv_hash / machines / expires_at` 供核对,便于管道使用。
278
-
279
- ### 3. 派发时携带 grant
280
-
281
- ```json
282
- POST /v1/task
283
- {
284
- "task_id": "01M5GRANTCLI0000000000000001",
285
- "mode": "replicate",
286
- "command_argv": ["node", "--version"],
287
- "cwd_rel": ".",
288
- "index_total": 1,
289
- "timeout_ms": 60000,
290
- "write": false,
291
- "grant": { ... /tmp/grant.json 内容 ... }
292
- }
293
- ```
294
-
295
- ### 4. 两处检查,各管一件事
296
-
297
- - **relay(派发预检)**:grant 的 `task_id` 必须与本次派发的 task 一致(`GRANT_TASK_MISMATCH`),`argv_hash` 必须与
298
- `command_argv` 一致(`GRANT_ARGV_MISMATCH`),`expires_at` 未过期(`GRANT_EXPIRED`)。这些是调用方错误(HTTP 400),
299
- 不是授权裁决。
300
- - **agent(真正的验证者)**:用机器上 pin 的操作员公钥验证签名、检查任务绑定/机器绑定/argv/有效期/max_uses,并把用过的
301
- grant 记入本地 ledger(重放拒绝为 `GRANT_REPLAY`)。空 allow-list + 合法 grant 会执行;同 argv 无 grant 仍
302
- `COMMAND_NOT_ALLOWED`。
303
-
304
- ### 5. 审计字段
305
-
306
- task summary 与 ledger 现在携带 `grant_id`、`argv_hash`、`grant_key_id`;项目事件里投影 `task.linked`,回答"这条命令是谁
307
- 授权、什么时候、在哪些机器上"。
308
-
309
- ### 6. 边界(仍不做的)
310
-
311
- - 不做长期委托/子授权:一次 grant 只覆盖一次执行。
312
- - 不做自动签发:操作员必须显式运行签发命令或调用等价 API。
313
- - 不做跨项目授权:grant 绑定机器与 argv,与项目无关。
314
-
315
- ## 撤销与审计(v1.3.12-beta.2)
316
-
317
- 撤销解决"签发之后才发现不该授权"的问题,不需要动已经发出的 grant 内容(它可能已在操作员本机或任务体里)。
318
-
319
- - **撤销一条**:`POST /v1/grants/revoke`,operator token,body `{grant_id, reason?}`。幂等:第二次撤销返回
320
- `already_revoked: true`,不报错。撤销写入 relay ledger(`grant.revoked`,含 reason 与操作者)。
321
- - **派发预检**:携带已撤销 grant 的任务在 `POST /v1/task` 阶段被拒:`403 GRANT_REVOKED`。机器不会收到 offer。
322
- - **审计视图**:`GET /v1/grants`,operator token。返回 `tasks`(所有携带过 grant 的任务:task_id / grant_id / argv_hash /
323
- key_id / mode / created_at)与 `revoked`(撤销记录)。只有 operator token 能读;device token 得到 401,因为这是授权台账,
324
- 不是机器的工作面。
325
- - **agent 侧边界**:agent 无法联系 relay 时仍按本地 ledger 执行已到手的 grant(正确性依赖 relay 派发,见 §4);撤销的作用
326
- 是阻止**新派发**,与"停止签发"同义。
327
-
328
- 撤销之后的新授权需要重新签发(新的 grant_id,绑定同一 task 或新 task)。
329
-
330
- ## 授权来源(v1.3.12-beta.3)
331
-
332
- 前两版解决了"能不能授权"和"能不能收回";这一版让结果自己回答"是谁授权的"。每个任务结果信封多一个字段:
333
-
334
- ```json
335
- {
336
- "authorization": {
337
- "provenance": "grant | allow-list | maintenance",
338
- "capability_id": "gr-…(grant 时)| maintenance request_id(maintenance 时)",
339
- "grant_id": "gr-…(grant 时)"
340
- }
341
- }
342
- ```
343
-
344
- - **allow-list**:机器所有者的长期决定,机器执行时命中静态表。
345
- - **grant**:操作员针对这一次执行的显式授权;附带 grant_id,可与 relay ledger 的 `grant.revoked` / 任务记录对账。
346
- - **maintenance**:签名维护请求;capability_id 是 request_id,对应 M1 的请求台账。
347
-
348
- 边界要写清楚:**provenance 是执行机器对自身判定的事实陈述,relay 只存储不验证**(signature 由 agent 验、派发由 relay 预检、撤销由
349
- relay 记账)。回答"这到底是谁允许的"仍以 relay ledger 为准;provenance 让每个结果可被机器与人快速定位到那条授权,而不是替代台账。
350
-
351
- ## 使用台账不可写(v1.3.12-beta.4)
352
-
353
- 真机发现:维护状态目录 `/var/lib/w2m/maintenance` 由 root 拥有且 0755,agent 以非特权用户运行;grant 第一次通过后写使用台账
354
- 失败(EACCES),异常冒泡成任务 crashed —— 表现是"授权了但一执行就崩"。这不是授权判定错误,是记录失败。
355
-
356
- 处理(fail closed):
357
-
358
- - `grant-gate` 把台账写入包在 try/catch 里,失败返回 `GRANT_LEDGER_UNAVAILABLE`,不执行未记录的一次授权(否则单次授权会
359
- 变成可重放的漏洞),也不再把整个 handler 打崩。
360
- - `bootstrap.sh` 把 `$BASE` 设为 `root:w2m 0775`(agent 需要在这里写 grant-uses.json);queue 仍为 0770、done/claimed 0750。
361
- - 回归用例:把 usedFile 指到不可写路径,断言返回该 code 而非抛异常。
@@ -1,70 +0,0 @@
1
- # M1 real-machine acceptance runbook (v1.3.10-beta.2+)
2
-
3
- > Goal: prove, on real machines, that a signed maintenance request updates/restarts/diagnoses a machine **without SSH and
4
- > without widening the allow-list**. This needs one privileged bootstrap per machine; after that everything is remote.
5
-
6
- ## 0. Prerequisites
7
-
8
- 1. The machine already runs the plugin (any version >= 1.3.10-beta.2) and the agent is paired.
9
- 2. The relay runs a version that carries the maintenance fields (**v1.3.10-beta.2 or later**). A 1.3.1 relay silently drops them.
10
- 3. You have one-time root (Linux) / Administrator (Windows) access to run the bootstrap script.
11
-
12
- ## 1. Bootstrap (once per machine)
13
-
14
- Linux:
15
-
16
- ~~~bash
17
- # copy the package to /opt/w2m/app first (deploy-w2m.sh), then:
18
- sudo bash /opt/w2m/app/deploy/maintenance/bootstrap.sh \
19
- --app-dir /opt/w2m/app \
20
- --machine-id <machine_id> \
21
- --operator-key /tmp/operator.pub.pem
22
- ~~~
23
-
24
- Windows (Administrator PowerShell):
25
-
26
- ~~~powershell
27
- powershell -ExecutionPolicy Bypass -File C:\w2m\app\deploy\maintenance\bootstrap.ps1 \
28
- -AppDir C:\w2m\app -MachineId <machine_id> -OperatorKey C:\w2m\operator.pub.pem
29
- ~~~
30
-
31
- The bootstrap installs a SYSTEM path/task that executes only update/restart/diag, creates the queue directories the agent
32
- writes to, and pins the operator public key. It never installs a shell.
33
-
34
- ## 2. Dispatch a signed request
35
-
36
- Use scripts/m1-acceptance.mjs on the control side:
37
-
38
- ~~~powershell
39
- $env:W2M_OPERATOR_TOKEN = '<operator token>'
40
- $env:W2M_DEVICE_TOKEN = '<this control machine device token>'
41
- node scripts/m1-acceptance.mjs --machine-id <target machine_id> --action diag
42
- ~~~
43
-
44
- Expected: task ends done, evidence file written under docs/acceptance/<version>/, and the target machine's
45
- /var/lib/w2m/maintenance/done/<request_id>.json holds the runner result.
46
-
47
- ## 3. Acceptance cases (record evidence for each)
48
-
49
- | Case | Request | Expected |
50
- |---|---|---|
51
- | M1-DIAG | action=diag | done; result sections include version/services/journal/disk |
52
- | M1-RESTART | action=restart targets=agent | done; machine heartbeats again |
53
- | M1-UPDATE | action=update version=<next> sha=<asset sha> | done; package.json version changes; app.old kept |
54
- | M1-UPDATE-ROLLBACK | same but broken sha/artifact | failed; previous tree still served (rollback) |
55
- | N1-EXPIRED | request with expires_at in the past | refused MAINTENANCE_EXPIRED, nothing executed |
56
- | N2-REPLAY | same request_id twice | second refused MAINTENANCE_REPLAY |
57
- | N3-TAMPER | change a param after signing | refused MAINTENANCE_BAD_SIGNATURE |
58
- | N4-WRONG-MACHINE | machine_id of another host | refused MAINTENANCE_WRONG_MACHINE |
59
-
60
- ## 4. What "green" means
61
-
62
- - Every positive case has a result file whose status is ok and whose assertions match the table.
63
- - Every negative case has a named refusal and **left the machine unchanged** (version, service state).
64
- - The ledger contains maintenance_summary for each task and the evidence JSON records task_id + assertions.
65
-
66
- ## 5. Known limitations to state in the release note
67
-
68
- - The bootstrap itself needs privilege; the channel removes SSH **after** it.
69
- - TOFU key pinning: first key seen wins until rotated by a signed rotation. Pairing-time pinning is the planned hardening.
70
- - No automatic rollback if the machine cannot be reached after restart; the operator re-dispatches restart (idempotent).