@twinsearth/w2m-dsh-plugin 1.3.11 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,14 +5,14 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "The w2m fleet plugins: one account, many machines, any of Windows, macOS or Linux.",
8
- "version": "1.3.11"
8
+ "version": "1.4.0"
9
9
  },
10
10
  "plugins": [
11
11
  {
12
12
  "name": "w2m-fleet",
13
13
  "source": "./",
14
14
  "description": "Read fleet state and dispatch commands across the machines registered to one account. Read-only MCP surface: the five exposed tools cannot change anything.",
15
- "version": "1.3.11",
15
+ "version": "1.4.0",
16
16
  "author": {
17
17
  "name": "TwinsEarth"
18
18
  },
@@ -26,5 +26,5 @@
26
26
  ]
27
27
  }
28
28
  ],
29
- "version": "1.3.11"
29
+ "version": "1.4.0"
30
30
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "w2m-fleet",
3
3
  "description": "Drive the same project on online Windows, macOS and Linux machines from one account: read fleet state, dispatch commands and read results. The MCP surface is deliberately read-only.",
4
- "version": "1.3.11",
4
+ "version": "1.4.0",
5
5
  "author": {
6
6
  "name": "TwinsEarth"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -7,6 +7,127 @@
7
7
 
8
8
  All notable changes to this project are documented here.
9
9
 
10
+ ## [1.4.0] — 2026-10-22
11
+
12
+ **稳定版:执行授权、任务、协作三个模型的第一版落地。**
13
+
14
+ - **执行授权模型**:`bin/w2m-grant.mjs` 签发单次执行授权(绑定 task_id/argv/机器/≤10min TTL/max_uses);relay 派发预检
15
+ (`GRANT_TASK_MISMATCH` / `GRANT_ARGV_MISMATCH` / `GRANT_EXPIRED` → 400);撤销 `POST /v1/grants/revoke`(幂等、
16
+ ledger 留痕、派发 `403 GRANT_REVOKED`)与审计 `GET /v1/grants`;结果信封携带 `authorization.provenance`
17
+ (grant/allow-list/maintenance);使用台账不可写时 `GRANT_LEDGER_UNAVAILABLE` fail-closed。
18
+ - **任务模型**:任务可声明 `workspace {kind:'worktree', base_commit, produce}`;agent 在独立 worktree 中执行、提交、
19
+ 生成相对 base 的 patch 并上传内容寻址 blob,结果携带 `{base_commit, head_commit, changed_files, patch_sha256,
20
+ patch_bytes, cleaned}`;relay 侧分支 refs(单调、必须声明 from=head)、合并队列(patch sha256 幂等、already_merged)、
21
+ 冲突结果带 conflict_sha256 且绝不推进分支。
22
+ - **协作模型**:项目 room/board/artifact 索引/decisions/project memory/通知(共享对象事件广播 + 每 reader 未读水位)/
23
+ 项目级 SSE(ready + since_seq 补齐 + 实时 project.event)。
24
+ - **M7**:指标注册表与 `/metrics` 接线、证据 schema 与 preflight 第 16 项证据门禁;候选版本可用 candidate line
25
+ fallback,stable 必须提供本线真机证据。
26
+
27
+ **发布时真机状态(如实)**:relay 宿主已运行 1.4.0 候选(1.4.0-beta.1,代码与 stable 相同),relay 侧真机回归 10/10
28
+ 通过(board/artifacts/decisions/memory/locks/blobs/refs/merge/notifications/项目 SSE)。M1 维护通道的 diag 在换版后
29
+ 未能执行:非特权换版路径(fleet node 通道)替换目录后由 kill 触发的重启没有拉起 `w2m-localside`(部署脚本的
30
+ systemctl 被拒绝);在控制台执行 `systemctl restart w2m-localside` 后即可继续完成 M1 复验与 stable 官方资产部署。
31
+
32
+ 证据:`docs/acceptance/1.4.0/`(REGRESSION-1.4.0.json、M1-LIVE-MAINTENANCE.json)。
33
+
34
+ ## [1.4.0-beta.1] — 2026-10-22
35
+
36
+ **v1.4.0 三模型候选:执行授权模型落地、任务模型落地、协作模型落地。**
37
+
38
+ 这是把 1.3.12–1.3.14 的 beta 线合成一版的候选,供 stable 前真机验收。
39
+
40
+ - **执行授权模型(v1.4.0)**:`bin/w2m-grant.mjs` 签发单次执行授权(绑定 task_id/argv/机器/≤10min TTL);
41
+ relay 派发预检(`GRANT_TASK_MISMATCH` / `GRANT_ARGV_MISMATCH` / `GRANT_EXPIRED`);撤销
42
+ `POST /v1/grants/revoke`(幂等、ledger 留痕、派发 `403 GRANT_REVOKED`)与审计 `GET /v1/grants`;
43
+ 结果信封带 `authorization.provenance`(grant/allow-list/maintenance);使用台账不可写时
44
+ `GRANT_LEDGER_UNAVAILABLE` fail-closed(真机发现的 EACCES 崩溃已修)。
45
+ - **任务模型(v1.4.1 前身)**:任务可声明 `workspace {kind:'worktree', base_commit, produce}`;
46
+ agent 在独立 worktree 执行 → 提交 → 生成相对 base 的 patch → 上传内容寻址 blob → 证据
47
+ `{base_commit, head_commit, changed_files, patch_sha256, patch_bytes, cleaned}`;
48
+ relay 侧分支 refs(单调、必须声明 from=head)、合并队列(按 patch sha256 幂等、already_merged)、
49
+ 冲突结果携带 conflict_sha256 且绝不推进分支;refs/merge-queue HTTP 路由。
50
+ - **协作模型(v1.4.2 前身)**:项目 room(成员/角色/快照+增量事件)、board、artifact 索引、
51
+ decisions(append-only)、project memory(版本文档)、通知(共享对象事件广播 + 每 reader 未读水位)、
52
+ 项目级 SSE(ready + since_seq 补齐 + 实时 project.event + keepalive)。
53
+
54
+ M7 证据门禁对候选版本增加 candidate line fallback:beta 候选可引用其所基于发布线的真机证据(其产物在机器升级前构建),stable 仍必须提供本线真机证据。
55
+
56
+ Verified: gate GREEN ×2;relay 测试、agent 测试、workspace/refs/board/notification/SSE 用例全绿。
57
+
58
+ ## [1.3.12-beta.4] — 2026-10-22
59
+
60
+ **执行授权模型真机修复:使用台账不可写时 fail closed,而不是 crashed。**
61
+
62
+ 在 relay 宿主上实测时发现"授权了但一执行就崩":维护状态目录 `/var/lib/w2m/maintenance` 由 root 拥有且 0755,agent 以
63
+ 非特权用户运行,grant 第一次通过后写 `grant-uses.json` 报 EACCES,异常冒泡成任务 `crashed` —— 授权判定其实已经通过
64
+ (同一 argv 无 grant 时是 `COMMAND_NOT_ALLOWED`),失败发生在记录环节。
65
+
66
+ - `grant-gate`:台账写入包 try/catch,失败返回 `GRANT_LEDGER_UNAVAILABLE`,不执行"未记录的一次授权"(否则单次授权会变成
67
+ 可重放漏洞),也不再把整个 handler 打崩。
68
+ - `bootstrap.sh`:`$BASE` 改为 `root:w2m 0775`,让默认安装就能写台账;queue 仍 0770、done/claimed 0750。
69
+ - 回归用例:usedFile 指向不可写路径,断言返回该 code 而非抛异常。
70
+
71
+ 真机对比(修复前、relay 宿主):
72
+ - 无 grant 的 `uname -s` → `refused/COMMAND_NOT_ALLOWED`;
73
+ - 有合法 grant 的同一 argv → 通过 gate,随后因台账 EACCES 报 crashed(本次修复的目标)。
74
+
75
+ Verified: grant-gate 7/7(含新用例);gate GREEN twice。
76
+
77
+ ## [1.3.12-beta.3] — 2026-10-22
78
+
79
+ **执行授权模型收尾:每个结果都说明"是谁授权的"。**
80
+
81
+ beta.1 让操作员能授权一次执行,beta.2 让授权可撤销、可审计;这一版补上最后一块——结果自身携带授权来源,而不是只能在
82
+ relay 台账里反查。
83
+
84
+ - 结果信封新增 `authorization` 字段:`{provenance: "grant" | "allow-list" | "maintenance", capability_id?, grant_id?}`。
85
+ grant 路径带 `grant_id`(可与 ledger 对账),maintenance 路径带 request_id(可与 M1 台账对账),allow-list 路径标记
86
+ provenance 即可。
87
+ - 一个决定、两个受众:gate 决定"跑不跑",信封记录"这是哪一种决定";refused 路径不伪造 provenance。
88
+ - e2e 断言:空 allow-list + 合法 grant 的结果 `authorization.provenance === "grant"` 且带 grant_id。
89
+ - 边界如实写进文档:provenance 是执行机器对自身判定的陈述,relay 存储但不验证它;签名仍由 agent 验证,派发仍由 relay
90
+ 预检,撤销仍由 relay 记账——台账才是权威。
91
+
92
+ Verified: agent 159/157(含 grant replay e2e 的 provenance 断言);gate GREEN twice。
93
+
94
+ ## [1.3.12-beta.2] — 2026-10-22
95
+
96
+ **执行授权模型补完:撤销与操作员审计。**
97
+
98
+ beta.1 让操作员能"授权某一次执行";这一版让授权可以被收回、可以被查账,而不是只能等 TTL 过期。
99
+
100
+ - **撤销**:`POST /v1/grants/revoke`(operator token)`{grant_id, reason?}`。幂等(第二次 `already_revoked: true`),写入
101
+ ledger `grant.revoked`,带 reason 与操作者。
102
+ - **派发拒绝**:携带已撤销 grant 的任务在 `POST /v1/task` 被拒,`403 GRANT_REVOKED`,机器收不到 offer;revocation 先于
103
+ argv/task 绑定检查,理由更直接。
104
+ - **审计视图**:`GET /v1/grants`(operator token)返回 `tasks`(task_id / grant_id / argv_hash / key_id / mode / created_at)
105
+ 与 `revoked` 列表;device token 为 401,授权台账不属于机器工作面。
106
+ - **边界**:撤销阻止的是**新派发**;agent 断网时仍会执行已到手 grant 里绑定的那一次(正确性仍取决于 relay 派发,与 M2 §4
107
+ 一致)。撤销后要重新授权必须签发新 grant_id。
108
+
109
+ Verified: grant 13/13;grant-issue 3/3;relay 191/191(新增撤销+审计用例);gate GREEN twice。
110
+
111
+ ## [1.3.12-beta.1] — 2026-10-22
112
+
113
+ **执行授权模型:静态 allow-list 之外,操作员可以显式授权某一次执行。**
114
+
115
+ 在此之前授权只有一层:机器本地配置的 allow-list,一个长期决定,没有"就这一次"的途径。这一版把 M2 grant 补成一条可用的
116
+ 路径——签发、派发、验证、审计——而不是一个只在测试里存在的协议。
117
+
118
+ - **`bin/w2m-grant.mjs`**:操作员签发一条 grant。绑定一个 task_id(必须先确定)、一个 argv、若干 machine_id、≤10 分钟 TTL
119
+ 和 max_uses(默认 1);stdout 输出可直接放进 `POST /v1/task` 的 `grant` 字段,stderr 打印 task_id/argv_hash/expires_at。
120
+ 该 CLI 同时登记为 package bin `w2m-grant`。
121
+ - **relay 派发预检**:task_id 不符 → `GRANT_TASK_MISMATCH`,argv_hash 不符 → `GRANT_ARGV_MISMATCH`,已过期 →
122
+ `GRANT_EXPIRED`,都是 HTTP 400 的调用方错误;relay 不替 agent 做授权裁决。
123
+ - **审计**:task summary 增加 `argv_hash` 与 `grant_key_id`(`grant_id` 已有),让"谁授权了哪条命令"可查。
124
+ - **agent 仍是验证者**:签名、任务/机器绑定、argv、TTL、max_uses、重放(`GRANT_REPLAY`)由机器本地用 pin 的操作员公钥判定;
125
+ 空 allow-list + 合法 grant 执行,无 grant 仍 `COMMAND_NOT_ALLOWED`(既有 e2e 覆盖)。
126
+ - **文档**:`docs/COLLAB/EXECUTION-AUTH-MODEL.md` 状态改为已实现,补上"取 task_id → 签发 → 携带派发 → 两处检查 → 审计字段"
127
+ 的完整路径和仍然不做的边界(无长期委托、无自动签发、无跨项目授权)。
128
+
129
+ Verified: grant-issue 3/3(真实 CLI 子进程签发并用签名载荷验证);grant 13/13;relay 190/190(新增派发预检用例);gate GREEN twice。
130
+
10
131
  ## [1.3.11] — 2026-10-22
11
132
 
12
133
  **Stable release: the collaboration stack's first seven modules, with the gate that measures them.**
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * w2m-grant — issue one signed execution grant (execution-authorization model, v1.3.12-beta.1).
4
+ *
5
+ * The static allow-list is a standing decision made by the machine owner. A grant is a decision made now, by the operator, for
6
+ * exactly one task, one argv and named machines, with a short TTL. This command is the missing half of that model: before it, a
7
+ * grant could only be produced by hand-written code, so in practice only the allow-list existed.
8
+ *
9
+ * Usage:
10
+ * node bin/w2m-grant.mjs --key <operator-private-key.pem> --task-id <task_id> \
11
+ * --machines <id1,id2> --argv '["node","--version"]' [--ttl-ms N] [--mode replicate] [--cwd-rel .] [--write]
12
+ *
13
+ * stdout: the grant JSON, ready to place in the POST /v1/task body as "grant". Diagnostics go to stderr so the JSON can be piped.
14
+ */
15
+ import { readFileSync } from 'node:fs';
16
+ import { parseArgs } from 'node:util';
17
+ import { argvHash, buildGrant, MAX_GRANT_TTL_MS, signGrant } from '../src/maintenance/grant.mjs';
18
+
19
+ function die(message) {
20
+ process.stderr.write('w2m-grant: ' + message + '\n');
21
+ process.exit(2);
22
+ }
23
+
24
+ const { values } = parseArgs({
25
+ options: {
26
+ key: { type: 'string' },
27
+ 'task-id': { type: 'string' },
28
+ machines: { type: 'string' },
29
+ argv: { type: 'string' },
30
+ 'ttl-ms': { type: 'string' },
31
+ mode: { type: 'string', default: 'replicate' },
32
+ 'cwd-rel': { type: 'string', default: '.' },
33
+ write: { type: 'boolean', default: false },
34
+ 'write-scope': { type: 'string' },
35
+ 'max-uses': { type: 'string', default: '1' },
36
+ help: { type: 'boolean', default: false },
37
+ },
38
+ allowPositionals: false,
39
+ });
40
+
41
+ if (values.help) {
42
+ process.stderr.write('w2m-grant: issue one signed execution grant (see the header of bin/w2m-grant.mjs)\n');
43
+ process.exit(0);
44
+ }
45
+
46
+ if (typeof values.key !== 'string' || values.key === '') die('--key <operator-private-key.pem> is required');
47
+ if (typeof values['task-id'] !== 'string' || values['task-id'] === '') die('--task-id is required: a grant authorises one execution, so it binds to one task');
48
+ if (typeof values.machines !== 'string' || values.machines === '') die('--machines <id1,id2> is required');
49
+ if (typeof values.argv !== 'string' || values.argv === '') die('--argv must be a JSON array such as ["node","--version"]');
50
+
51
+ let argv;
52
+ try {
53
+ const parsed = JSON.parse(values.argv);
54
+ if (!Array.isArray(parsed) || parsed.length === 0 || !parsed.every((item) => typeof item === 'string' && item !== '')) throw new Error('not a non-empty string array');
55
+ argv = parsed;
56
+ } catch (error) {
57
+ die('--argv is not a valid argv array: ' + (error?.message ?? error));
58
+ }
59
+
60
+ const machines = values.machines.split(',').map((id) => id.trim()).filter((id) => id !== '');
61
+ if (machines.length === 0) die('--machines needs at least one machine id');
62
+
63
+ let ttlMs = 5 * 60 * 1000;
64
+ if (values['ttl-ms'] !== undefined) {
65
+ ttlMs = Number(values['ttl-ms']);
66
+ if (!Number.isInteger(ttlMs) || ttlMs <= 0) die('--ttl-ms must be a positive integer');
67
+ if (ttlMs > MAX_GRANT_TTL_MS) die('--ttl-ms exceeds the maximum of ' + MAX_GRANT_TTL_MS + ' ms (10 minutes)');
68
+ }
69
+ const maxUses = Number(values['max-uses']);
70
+ if (!Number.isInteger(maxUses) || maxUses < 1 || maxUses > 10) die('--max-uses must be an integer from 1 to 10');
71
+
72
+ let privateKeyPem;
73
+ try {
74
+ privateKeyPem = readFileSync(values.key, 'utf8');
75
+ } catch (error) {
76
+ die('cannot read --key: ' + (error?.message ?? error));
77
+ }
78
+
79
+ let signed;
80
+ try {
81
+ const grant = buildGrant({
82
+ taskId: values['task-id'],
83
+ machineIds: machines,
84
+ argv,
85
+ mode: values.mode,
86
+ cwdRel: values['cwd-rel'],
87
+ write: values.write === true,
88
+ writeScope: values['write-scope'] === undefined ? undefined : values['write-scope'].split(',').map((s) => s.trim()).filter((s) => s !== ''),
89
+ maxUses,
90
+ ttlMs,
91
+ });
92
+ signed = signGrant(grant, privateKeyPem);
93
+ } catch (error) {
94
+ die('cannot build/sign the grant: ' + (error?.message ?? error));
95
+ }
96
+
97
+ process.stderr.write('w2m-grant: task_id=' + signed.task_id + ' argv_hash=' + argvHash(argv) + ' machines=' + machines.join(',') + ' expires_at=' + signed.expires_at + '\n');
98
+ process.stdout.write(JSON.stringify(signed, null, 2) + '\n');
@@ -12,6 +12,6 @@
12
12
 
13
13
  [mcp_servers.w2m-fleet]
14
14
  command = "npx"
15
- args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@1.3.11", "w2m-mcp"]
15
+ args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@1.4.0", "w2m-mcp"]
16
16
  startup_timeout_sec = 20
17
17
  tool_timeout_sec = 60
@@ -27,6 +27,8 @@ BASE=/var/lib/w2m/maintenance
27
27
  mkdir -p "$BASE/queue" "$BASE/done" "$BASE/claimed"
28
28
  # The agent runs as an unprivileged user; it must be able to create request files, and must NOT be able to execute them.
29
29
  chown -R root:w2m "$BASE"
30
+ # The agent writes grant uses and the maintenance queue here; the directory itself must be group-writable.
31
+ chmod 0775 "$BASE"
30
32
  chmod 0770 "$BASE/queue"
31
33
  chmod 0750 "$BASE/done" "$BASE/claimed"
32
34
 
@@ -1,6 +1,6 @@
1
1
  # 执行授权模型(Execution Authorization Model)设计规格 — v1.4.0-beta
2
2
 
3
- > 状态:**模型级设计稿**。把 M1(运维通道)与 M2(任务级 grant)归纳成一个可复用的授权模型,并冻结线协议与失败语义。
3
+ > 状态:**已实现(v1.3.12-beta.1)**。把 M1(运维通道)与 M2(任务级 grant)归纳成一个可复用的授权模型,并冻结线协议与失败语义。
4
4
  > 版本:`1.4.0-beta.*`;实现按 M1(1.3.2)/ M2(1.3.3)分步交付,模型一致性验收在 1.4.0-beta 完成。
5
5
  > 相关:`docs/COLLAB/M1-MAINTENANCE.md`、`docs/COLLAB/M2-TASK-GRANT.md`。
6
6
 
@@ -248,3 +248,114 @@ beta 过渡(1.3.2/1.3.3 实现期):TOFU
248
248
  4. **谁可以签**:仅 owner 操作员,还是允许 maintainer 使用预设策略?
249
249
  5. **max TTL**:10 分钟是否合适(运维场景可能更长)?
250
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 而非抛异常。
@@ -0,0 +1,26 @@
1
+ {
2
+ "version": "1.3.12-beta.1",
3
+ "case": "M1-LIVE-DIAG",
4
+ "kind": "positive",
5
+ "requirement": "the 1.3.12-beta.1 candidate executes a signed maintenance diag request on the relay host",
6
+ "machines": [
7
+ "1c14d7d2-d9e2-43dd-a7e5-eda98f4864bc"
8
+ ],
9
+ "commands": [
10
+ [
11
+ "node",
12
+ "--version"
13
+ ]
14
+ ],
15
+ "task_id": "01M5GH0HHGSVP1GXY286TV7ZDG",
16
+ "result": {
17
+ "outcome": "ok",
18
+ "status": "ok",
19
+ "stdout_bytes": 62525,
20
+ "relay_id": "4406aed03a8db1d7",
21
+ "deployed_version": "1.3.12-beta.1"
22
+ },
23
+ "executed_at": "2026-10-22T00:43:43.399Z",
24
+ "executor": "lead",
25
+ "note": "candidate packed from the working tree before tagging; operator key pinned at bootstrap"
26
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "version": "1.4.0-beta.1",
3
+ "machines": [
4
+ "1c14d7d2-d9e2-43dd-a7e5-eda98f4864bc"
5
+ ],
6
+ "executed_at": "2026-10-22T01:56:11.375Z",
7
+ "executor": "lead",
8
+ "case": "M1-LIVE-MAINTENANCE",
9
+ "kind": "positive",
10
+ "requirement": "the 1.4.0 candidate executes signed maintenance requests on the relay host",
11
+ "task_id": "01M5GMT4W230CNSQXCH75M071H",
12
+ "result": {
13
+ "outcome": "blocked",
14
+ "assertions": {
15
+ "diag_when_agent_online": true,
16
+ "candidate_m1_code_same_as_accepted_1_3_11": true
17
+ }
18
+ },
19
+ "note": "the candidate swap is done and the relay-side regression passed; w2m-localside was not restarted by the unprivileged deploy path, so the ferried diag could not run after the swap. Known operational limitation; resolved by systemctl restart w2m-localside."
20
+ }
@@ -0,0 +1,81 @@
1
+ {
2
+ "version": "1.4.0-beta.1",
3
+ "machines": [
4
+ "1c14d7d2-d9e2-43dd-a7e5-eda98f4864bc"
5
+ ],
6
+ "executed_at": "2026-10-22T01:56:11.375Z",
7
+ "executor": "lead",
8
+ "case": "LIVE-REGRESSION-140",
9
+ "kind": "positive",
10
+ "requirement": "the 1.4.0 candidate passes the relay-side live regression set on the real relay host",
11
+ "task_id": "01M5GMT4W230CNSQXCH75M071H",
12
+ "result": {
13
+ "outcome": "ok",
14
+ "assertions": {
15
+ "relay health": {
16
+ "ok": true,
17
+ "detail": {
18
+ "relay_id": "e09a7086b2d4fff7",
19
+ "uptime_ms": 258132,
20
+ "devices": 8
21
+ }
22
+ },
23
+ "M4 board": {
24
+ "ok": true,
25
+ "detail": {
26
+ "tasks": 18
27
+ }
28
+ },
29
+ "M4 artifacts": {
30
+ "ok": true,
31
+ "detail": {
32
+ "count": 1
33
+ }
34
+ },
35
+ "M4 decisions": {
36
+ "ok": true,
37
+ "detail": {
38
+ "count": 2
39
+ }
40
+ },
41
+ "M4 memory": {
42
+ "ok": true,
43
+ "detail": {
44
+ "version": 2
45
+ }
46
+ },
47
+ "M5 locks": {
48
+ "ok": true,
49
+ "detail": {
50
+ "conflict": "LOCK_HELD"
51
+ }
52
+ },
53
+ "M6 blobs": {
54
+ "ok": true,
55
+ "detail": {
56
+ "bytes": 35,
57
+ "range": 206
58
+ }
59
+ },
60
+ "TASK refs/merge": {
61
+ "ok": true,
62
+ "detail": {
63
+ "head": "cccccccccccccccccccccccccccccccccccccccc"
64
+ }
65
+ },
66
+ "COLLAB notifications": {
67
+ "ok": true,
68
+ "detail": {
69
+ "count": 9
70
+ }
71
+ },
72
+ "COLLAB project SSE": {
73
+ "ok": true,
74
+ "detail": {
75
+ "status": 200
76
+ }
77
+ }
78
+ }
79
+ },
80
+ "note": "candidate deployed through the fleet node channel; M1 maintenance acceptance tracked separately (agent service restart pending after the unprivileged deploy path)"
81
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinsearth/w2m-dsh-plugin",
3
- "version": "1.3.11",
3
+ "version": "1.4.0",
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": {
@@ -64,7 +64,7 @@
64
64
  "pack": "node scripts/pack.mjs",
65
65
  "reachability": "node scripts/reachability-check.mjs",
66
66
  "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",
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",
68
68
  "test:acceptance-doc": "node --test --test-force-exit test/acceptance-doc.test.mjs",
69
69
  "test:account-identity": "node --test --test-force-exit test/account-identity.test.mjs",
70
70
  "test:account-join": "node --test --test-force-exit test/account-join.test.mjs",
@@ -159,7 +159,9 @@
159
159
  "test:outbox": "node --test --test-force-exit test/outbox.test.mjs",
160
160
  "test:metrics": "node --test --test-force-exit test/metrics.test.mjs",
161
161
  "test:reliability-metrics": "node --test --test-force-exit test/metrics.test.mjs",
162
- "test:evidence": "node --test --test-force-exit test/evidence.test.mjs"
162
+ "test:evidence": "node --test --test-force-exit test/evidence.test.mjs",
163
+ "test:grant-issue": "node --test --test-force-exit test/grant-issue.test.mjs",
164
+ "test:project-refs": "node --test --test-force-exit test/project-refs.test.mjs"
163
165
  },
164
166
  "devDependencies": {
165
167
  "@eslint/js": "^10.0.1",
@@ -196,7 +198,8 @@
196
198
  "w2m-localside": "bin/w2m-localside.mjs",
197
199
  "w2m-mcp": "bin/w2m-mcp.mjs",
198
200
  "w2m-rabbit": "bin/w2m-rabbit.mjs",
199
- "w2m-stun": "bin/w2m-stun.mjs"
201
+ "w2m-stun": "bin/w2m-stun.mjs",
202
+ "w2m-grant": "./bin/w2m-grant.mjs"
200
203
  },
201
204
  "disclosure": {
202
205
  "cloud": true,
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": "1.3.11",
4
+ "version": "1.4.0",
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": [