@twinsearth/w2m-dsh-plugin 1.9.15 → 1.9.17
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 +21 -0
- package/bin/w2m-joint.mjs +49 -5
- package/bin/w2m-scrub.mjs +78 -0
- package/codex/mcp-servers.toml +1 -1
- package/docs/acceptance/1.9.16/RELEASE-1916.json +21 -0
- package/docs/acceptance/1.9.17/RELEASE-1917.json +20 -0
- package/docs/acceptance/2.0.0-beta.1/RELEASE-200B1.json +23 -0
- package/docs/acceptance-ops/2026-10-28/G0-3-106.53.197.47.json +35 -0
- package/docs/acceptance-ops/2026-10-28/G0-3-202.182.123.154.json +35 -0
- package/docs/acceptance-ops/2026-10-28/G0-3-43.142.74.71.json +35 -0
- package/docs/acceptance-ops/2026-10-28/R1-OPERATOR-TOKEN.json +19 -0
- package/docs/acceptance-ops/2026-10-28/R3-CAPABILITIES.json +51 -0
- package/docs/acceptance-ops/2026-10-28/R8-LEDGER-JOURNAL.json +21 -0
- package/lib/client.js +393 -367
- package/package.json +4 -3
- package/plugin.json +1 -1
- package/src/plugin/joint-dev/plan-store.mjs +117 -32
- package/src/plugin/joint-dev/relay-dispatch.mjs +89 -0
- package/src/plugin/operator-key.mjs +73 -0
- package/src/plugin/operator-token-route.mjs +71 -0
- package/src/plugin/relay-capabilities.mjs +66 -0
- package/src/plugin/tools.mjs +6 -2
- package/src/relay/offer-journal.mjs +71 -0
- package/src/relay/p0-projection.mjs +10 -0
- package/src/relay/report.mjs +5 -2
- package/src/relay/scrub.mjs +94 -81
- package/src/relay/server.mjs +14 -1
- package/src/relay/state.mjs +3340 -3300
|
@@ -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.9.
|
|
8
|
+
"version": "1.9.17"
|
|
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.9.
|
|
15
|
+
"version": "1.9.17",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "TwinsEarth"
|
|
18
18
|
},
|
|
@@ -26,5 +26,5 @@
|
|
|
26
26
|
]
|
|
27
27
|
}
|
|
28
28
|
],
|
|
29
|
-
"version": "1.9.
|
|
29
|
+
"version": "1.9.17"
|
|
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.9.
|
|
4
|
+
"version": "1.9.17",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TwinsEarth"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,27 @@
|
|
|
7
7
|
|
|
8
8
|
All notable changes to this project are documented here.
|
|
9
9
|
|
|
10
|
+
## [1.9.17] - 2026-10-28
|
|
11
|
+
|
|
12
|
+
- 修复左侧栏 GPW 入口只剩小图标:恢复 🌐 GPW 文字(侧栏 label 仍为空,不会重复)。
|
|
13
|
+
- 对话全文搜索高亮加强:命中项绿色文字 + 黄色背景(::highlight(w2m-conversation-search) 与 .w2mSearchMark 双样式),主工作区正文中直接可见;显示 当前/总数,Enter/Shift+Enter 或 ↑/↓ 跳转,Esc 清除。
|
|
14
|
+
|
|
15
|
+
### [2.0.0-beta.1](pre-release) - 2026-10-28
|
|
16
|
+
|
|
17
|
+
首个联合开发 beta(S0–S1)。
|
|
18
|
+
|
|
19
|
+
- S0 事实基座:operator token 安全存储(0600 文件 + 指纹状态)、relay /healthz 广告 relay_version/features、客户端能力协商 fail-closed;版本偏斜策略按“三机同版本才允许联合开发、enforce 不开启”执行。
|
|
20
|
+
- S1 选择真实生效:派发前按当前设备名册重校验被选机器(离线 → TARGET_UNAVAILABLE,不换机);派发后按 relay leases 校验,未被点名的机器也执行 → 取消任务并 SELECTION_NOT_HONOURED。
|
|
21
|
+
- 联合开发操作员入口:w2m-joint(plan create/list/get、approve、status、dispatch --dry-run,能力协商接入);联合开发默认关闭,属实验性 beta。
|
|
22
|
+
- 运维工具:w2m-scrub(dry-run/apply,递归清点 blobs/<aa>/<sha>,备份必须外置,二次幂等),用于旧 relay 清淤。
|
|
23
|
+
- beta 发布:仅 GitHub prerelease;npm 不发布本 beta。
|
|
24
|
+
|
|
25
|
+
## [1.9.16] - 2026-10-28
|
|
26
|
+
|
|
27
|
+
- G0-2:relay /healthz 新增 relay_version 与 features(target-machines / result-projection / guard-v1 / relay-version-v1);w2m_status 显示这两项,便于核对三台 relay 的实际版本与能力。
|
|
28
|
+
- 新增 relay-capabilities 模块与 w2m-joint dispatch 能力协商:目标 relay 缺少必需 feature(或旧 relay 完全不广告)时,联合派发 fail-closed 返回 RELAY_CAPABILITIES_MISSING,不靠“先发再发现”。
|
|
29
|
+
- 新增 test/relay-health-capabilities.test.mjs(5 项)。
|
|
30
|
+
|
|
10
31
|
## [1.9.15] - 2026-10-28
|
|
11
32
|
|
|
12
33
|
- G0-1:operator token 安全存储。新增 `~/.w2m/operator-token`(0600,可用 W2M_OPERATOR_TOKEN_FILE 覆盖)与 `w2m-operator` CLI(set 从 stdin/文件读取、status 只显示指纹、clear);w2m_status 新增 operatorToken_fingerprint/operatorToken_source,token 值不进日志/状态。
|
package/bin/w2m-joint.mjs
CHANGED
|
@@ -20,9 +20,14 @@
|
|
|
20
20
|
import { readFileSync } from 'node:fs';
|
|
21
21
|
|
|
22
22
|
import { buildControlEnvelopes, buildPlan } from '../src/plugin/joint-dev/dispatch.mjs';
|
|
23
|
+
import { negotiateFeatures, readRelayCapabilities } from '../src/plugin/relay-capabilities.mjs';
|
|
24
|
+
import { dispatchPlanToRelay } from '../src/plugin/joint-dev/relay-dispatch.mjs';
|
|
25
|
+
import { readOperatorToken } from '../src/plugin/operator-token.mjs';
|
|
26
|
+
import { ensureOperatorKey, signApprovalPayload } from '../src/plugin/operator-key.mjs';
|
|
23
27
|
import {
|
|
24
28
|
approvePlan,
|
|
25
29
|
assertDispatchAllowed,
|
|
30
|
+
consumeApproval,
|
|
26
31
|
listPlans,
|
|
27
32
|
loadApproval,
|
|
28
33
|
loadPlan,
|
|
@@ -54,7 +59,7 @@ if (stateDir === null) fail('STATE_DIR_REQUIRED', 'pass --state <dir> (the joint
|
|
|
54
59
|
if (command === 'plan' && action === 'create') {
|
|
55
60
|
if (typeof args.steps !== 'string') fail('STEPS_REQUIRED', 'pass --steps <steps.json>');
|
|
56
61
|
let steps;
|
|
57
|
-
try { steps = JSON.parse(readFileSync(args.steps, 'utf8').replace(
|
|
62
|
+
try { steps = JSON.parse(readFileSync(args.steps, 'utf8').replace(/^\uFEFF/, '')); } catch (error) {
|
|
58
63
|
fail('STEPS_UNREADABLE', error instanceof Error ? error.message : String(error));
|
|
59
64
|
}
|
|
60
65
|
if (!Array.isArray(steps)) fail('STEPS_INVALID', 'steps.json must be a JSON array');
|
|
@@ -98,6 +103,10 @@ if (command === 'approve') {
|
|
|
98
103
|
const authority = Number.parseInt(String(args.authority ?? ''), 10);
|
|
99
104
|
if (!Number.isInteger(authority)) fail('AUTHORITY_REQUIRED', 'pass --authority <0..100>');
|
|
100
105
|
const ttlMin = Number.parseInt(String(args['ttl-min'] ?? '10'), 10);
|
|
106
|
+
// decision 1/2: approvals are signed by the local operator Ed25519 key; the private key never
|
|
107
|
+
// leaves this machine, and only its fingerprint is printed.
|
|
108
|
+
const key = ensureOperatorKey();
|
|
109
|
+
if (key.ok !== true) fail(key.code ?? 'OPERATOR_KEY_MISSING', key.detail ?? null);
|
|
101
110
|
const approved = approvePlan({
|
|
102
111
|
stateDir,
|
|
103
112
|
planSha: args.plan,
|
|
@@ -105,9 +114,14 @@ if (command === 'approve') {
|
|
|
105
114
|
authority,
|
|
106
115
|
note: typeof args.note === 'string' ? args.note : '',
|
|
107
116
|
ttlMs: (Number.isFinite(ttlMin) ? ttlMin : 10) * 60_000,
|
|
117
|
+
signer: (body) => ({
|
|
118
|
+
sig: signApprovalPayload(body, key.privateKey),
|
|
119
|
+
key_id: key.key_id,
|
|
120
|
+
public_key_pem: key.publicKeyPem,
|
|
121
|
+
}),
|
|
108
122
|
});
|
|
109
123
|
if (approved.ok !== true) fail(approved.code ?? 'APPROVAL_FAILED', approved.detail ?? null);
|
|
110
|
-
print({ ok: true, approval: approved.approval, path: approved.path });
|
|
124
|
+
print({ ok: true, approval: approved.approval, path: approved.path, signed: approved.signed === true, operator_key_id: key.key_id });
|
|
111
125
|
process.exit(0);
|
|
112
126
|
}
|
|
113
127
|
|
|
@@ -116,12 +130,13 @@ if (command === 'status') {
|
|
|
116
130
|
const plan = loadPlan({ stateDir, planSha: args.plan });
|
|
117
131
|
if (plan.ok !== true) fail(plan.code ?? 'PLAN_NOT_FOUND');
|
|
118
132
|
const approval = loadApproval({ stateDir, planSha: args.plan });
|
|
119
|
-
const gate = assertDispatchAllowed({ stateDir, planSha: args.plan });
|
|
133
|
+
const gate = assertDispatchAllowed({ stateDir, planSha: args.plan, requireSignature: args['allow-unsigned-approval'] !== true });
|
|
120
134
|
print({
|
|
121
135
|
ok: true,
|
|
122
136
|
plan_sha256: plan.plan.plan_sha256,
|
|
123
137
|
steps: plan.plan.steps.map((step) => ({ step_id: step.step_id, machine_id: step.machine_id, command_hash: step.command_hash })),
|
|
124
138
|
approval: approval.ok ? { approver: approval.approval.approver, authority: approval.approval.authority, expires_at: approval.approval.expires_at } : { state: approval.code },
|
|
139
|
+
approval_strength: gate.strength ?? null,
|
|
125
140
|
dispatch_allowed: gate.ok === true,
|
|
126
141
|
blocked_by: gate.ok === true ? null : gate.code,
|
|
127
142
|
});
|
|
@@ -130,15 +145,44 @@ if (command === 'status') {
|
|
|
130
145
|
|
|
131
146
|
if (command === 'dispatch') {
|
|
132
147
|
if (typeof args.plan !== 'string') fail('PLAN_REQUIRED', 'pass --plan <sha256>');
|
|
133
|
-
const
|
|
148
|
+
const key = ensureOperatorKey();
|
|
149
|
+
const trusted = key.ok === true ? [key.key_id] : [];
|
|
150
|
+
const gate = assertDispatchAllowed({ stateDir, planSha: args.plan, requireSignature: args['allow-unsigned-approval'] !== true, trustedOperatorKeyIds: trusted });
|
|
134
151
|
if (gate.ok !== true) fail(gate.code ?? 'APPROVAL_REQUIRED', 'dispatch refused before anything was sent');
|
|
152
|
+
const relayUrl = typeof args.relay === 'string' ? args.relay : null;
|
|
135
153
|
if (args['dry-run'] !== true) {
|
|
136
|
-
fail('
|
|
154
|
+
if (relayUrl === null) fail('RELAY_REQUIRED', 'real dispatch needs --relay <url>');
|
|
155
|
+
let token = null;
|
|
156
|
+
if (typeof args['token-file'] === 'string') {
|
|
157
|
+
token = readFileSync(args['token-file'], 'utf8').trim();
|
|
158
|
+
} else {
|
|
159
|
+
const secure = readOperatorToken();
|
|
160
|
+
token = secure.configured === true ? secure.token : null;
|
|
161
|
+
}
|
|
162
|
+
if (token === null) fail('OPERATOR_TOKEN_MISSING', 'run w2m-operator set --file <token file>, or pass --token-file');
|
|
163
|
+
// decision 2: one approval authorises exactly one dispatch.
|
|
164
|
+
const consumed = consumeApproval({ stateDir, planSha: args.plan, approvalSha: gate.approval.approval.approval_sha256 });
|
|
165
|
+
if (consumed.ok !== true) fail(consumed.code ?? 'APPROVAL_CONSUME_FAILED', consumed.detail ?? null);
|
|
166
|
+
const sent = await dispatchPlanToRelay({ rabbitUrl: relayUrl, token, plan: gate.plan, approval: gate.approval.approval });
|
|
167
|
+
print({ ...sent, plan_sha256: gate.plan.plan_sha256, relay: relayUrl });
|
|
168
|
+
process.exit(sent.ok === true ? 0 : 1);
|
|
169
|
+
}
|
|
170
|
+
let capabilities = null;
|
|
171
|
+
let negotiation = null;
|
|
172
|
+
if (typeof args.relay === 'string') {
|
|
173
|
+
capabilities = await readRelayCapabilities({ rabbitUrl: args.relay });
|
|
174
|
+
negotiation = negotiateFeatures(capabilities);
|
|
175
|
+
if (negotiation.ok !== true) {
|
|
176
|
+
fail(negotiation.code ?? 'RELAY_CAPABILITIES_MISSING', { missing: negotiation.missing ?? null, relay: args.relay });
|
|
177
|
+
}
|
|
137
178
|
}
|
|
138
179
|
const envelopes = buildControlEnvelopes(gate.plan);
|
|
139
180
|
print({
|
|
140
181
|
ok: true,
|
|
141
182
|
dry_run: true,
|
|
183
|
+
relay: typeof args.relay === 'string' ? args.relay : null,
|
|
184
|
+
relay_capabilities: capabilities === null ? null : { relay_version: capabilities.relay_version, features: capabilities.features },
|
|
185
|
+
relay_negotiation: negotiation,
|
|
142
186
|
plan_sha256: gate.plan.plan_sha256,
|
|
143
187
|
approval_sha256: gate.approval.approval_sha256,
|
|
144
188
|
envelopes: envelopes.length,
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Relay state scrub CLI (G0-3).
|
|
4
|
+
*
|
|
5
|
+
* Moves the data-plane residue out of a relay's live state: ledger lines are projected (content
|
|
6
|
+
* fields dropped), the content-addressed blob tree (the real layout is blobs/<aa>/<sha>) is deleted,
|
|
7
|
+
* and the result is scanned for any surviving content marker.
|
|
8
|
+
*
|
|
9
|
+
* Dry-run is the default. `--apply` requires `--backup-dir` OUTSIDE the state directory: the bytes
|
|
10
|
+
* the scrub removes must still exist somewhere the operator controls, or this is deletion, not
|
|
11
|
+
* cleanup with a floor.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* node bin/w2m-scrub.mjs --state /var/lib/w2m/rabbit
|
|
15
|
+
* node bin/w2m-scrub.mjs --state /var/lib/w2m/rabbit --apply --backup-dir /root/w2m-backups
|
|
16
|
+
*
|
|
17
|
+
* Exit codes: 0 ok, 1 refusal, 2 usage.
|
|
18
|
+
*/
|
|
19
|
+
import { cpSync, existsSync, mkdirSync } from 'node:fs';
|
|
20
|
+
import { join, resolve } from 'node:path';
|
|
21
|
+
|
|
22
|
+
import { scanForContent, scrubBlobs, scrubLedger } from '../src/relay/scrub.mjs';
|
|
23
|
+
|
|
24
|
+
const args = process.argv.slice(2);
|
|
25
|
+
const flag = (name) => { const index = args.indexOf(name); return index >= 0 ? args[index + 1] : undefined; };
|
|
26
|
+
const has = (name) => args.includes(name);
|
|
27
|
+
const print = (value) => process.stdout.write(JSON.stringify(value, null, 2) + '\n');
|
|
28
|
+
|
|
29
|
+
const stateDir = flag('--state');
|
|
30
|
+
if (typeof stateDir !== 'string') {
|
|
31
|
+
print({ ok: false, code: 'STATE_REQUIRED', usage: '--state <dir> [--apply --backup-dir <dir>]' });
|
|
32
|
+
process.exit(2);
|
|
33
|
+
}
|
|
34
|
+
const apply = has('--apply');
|
|
35
|
+
const backupDir = flag('--backup-dir');
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
|
|
38
|
+
if (apply === true) {
|
|
39
|
+
if (typeof backupDir !== 'string' || backupDir === '') {
|
|
40
|
+
print({ ok: false, code: 'BACKUP_DIR_REQUIRED', detail: '--apply requires --backup-dir outside the state directory' });
|
|
41
|
+
process.exit(2);
|
|
42
|
+
}
|
|
43
|
+
if (resolve(backupDir).startsWith(resolve(stateDir) + '/') || resolve(backupDir) === resolve(stateDir)) {
|
|
44
|
+
print({ ok: false, code: 'BACKUP_DIR_INSIDE_STATE', detail: 'the backup must live outside the relay state directory' });
|
|
45
|
+
process.exit(2);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const stamp = new Date(now).toISOString().replace(/[:.]/g, '-');
|
|
50
|
+
let backupRoot = null;
|
|
51
|
+
if (apply === true) {
|
|
52
|
+
backupRoot = join(backupDir, 'relay-' + stamp);
|
|
53
|
+
try {
|
|
54
|
+
mkdirSync(backupRoot, { recursive: true });
|
|
55
|
+
for (const name of ['ledger.jsonl', 'blobs', 'results', 'devices.json']) {
|
|
56
|
+
const source = join(stateDir, name);
|
|
57
|
+
if (existsSync(source)) cpSync(source, join(backupRoot, name), { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
print({ ok: false, code: 'BACKUP_FAILED', detail: error instanceof Error ? error.message : String(error) });
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const ledger = scrubLedger(join(stateDir, 'ledger.jsonl'), { dryRun: apply !== true, now });
|
|
66
|
+
const blobs = scrubBlobs(join(stateDir, 'blobs'), { dryRun: apply !== true, now });
|
|
67
|
+
const scan = apply === true ? scanForContent(stateDir) : { ok: null, hits: [] };
|
|
68
|
+
|
|
69
|
+
print({
|
|
70
|
+
ok: ledger.ok === true && blobs.ok === true && (apply !== true || scan.ok === true),
|
|
71
|
+
apply,
|
|
72
|
+
state: stateDir,
|
|
73
|
+
backup_dir: backupRoot,
|
|
74
|
+
ledger: { exists: ledger.exists, changed: ledger.changed, lines: ledger.lines, backup: ledger.backup },
|
|
75
|
+
blobs: { exists: blobs.exists, files: blobs.files?.length ?? 0, bytes: blobs.bytes, deleted: blobs.deleted },
|
|
76
|
+
scan: { ok: scan.ok, hits: scan.hits?.length ?? 0 },
|
|
77
|
+
});
|
|
78
|
+
process.exit(ledger.ok === true && blobs.ok === true && (apply !== true || scan.ok === true) ? 0 : 1);
|
package/codex/mcp-servers.toml
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.9.16",
|
|
3
|
+
"case": "RELEASE-1916",
|
|
4
|
+
"kind": "positive",
|
|
5
|
+
"machines": [
|
|
6
|
+
"b53a37f4-1464-4512-9259-33a7df091e3e"
|
|
7
|
+
],
|
|
8
|
+
"executed_at": "2026-10-28T01:20:53.202Z",
|
|
9
|
+
"executor": "lead",
|
|
10
|
+
"task_id": "pre-release-1916-1793150453204",
|
|
11
|
+
"requirement": "V1.9.16:relay 版本/能力广告与联合派发能力协商 fail-closed",
|
|
12
|
+
"result": {
|
|
13
|
+
"outcome": "ok",
|
|
14
|
+
"assertions": {
|
|
15
|
+
"healthz": "relay_version + features[]",
|
|
16
|
+
"status": "w2m_status exposes relay_version/features",
|
|
17
|
+
"negotiation": "joint dispatch refuses RELAY_CAPABILITIES_MISSING on old relay or missing feature",
|
|
18
|
+
"tests": "relay-health-capabilities 5/5"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.9.17",
|
|
3
|
+
"case": "RELEASE-1917",
|
|
4
|
+
"kind": "positive",
|
|
5
|
+
"machines": [
|
|
6
|
+
"b53a37f4-1464-4512-9259-33a7df091e3e"
|
|
7
|
+
],
|
|
8
|
+
"executed_at": "2026-10-28T03:57:30.515Z",
|
|
9
|
+
"executor": "lead",
|
|
10
|
+
"task_id": "pre-release-1917-1793159850517",
|
|
11
|
+
"requirement": "V1.9.17:左侧栏 GPW 文字恢复 + 搜索命中绿色文字/黄色背景",
|
|
12
|
+
"result": {
|
|
13
|
+
"outcome": "ok",
|
|
14
|
+
"assertions": {
|
|
15
|
+
"sidebar": "🌐 GPW(组件内文字,panellist label 仍为空)",
|
|
16
|
+
"search": "::highlight 与 .w2mSearchMark 双样式:color #0b7a33, background #ffeb3b",
|
|
17
|
+
"tests": "client-bundle 26/26"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "2.0.0-beta.1",
|
|
3
|
+
"case": "RELEASE-200B1",
|
|
4
|
+
"kind": "positive",
|
|
5
|
+
"machines": [
|
|
6
|
+
"b53a37f4-1464-4512-9259-33a7df091e3e"
|
|
7
|
+
],
|
|
8
|
+
"executed_at": "2026-10-28T01:38:12.535Z",
|
|
9
|
+
"executor": "lead",
|
|
10
|
+
"task_id": "pre-release-200b1-1793151492536",
|
|
11
|
+
"requirement": "v2.0.0-beta.1(S0–S1):版本/能力协商、operator token 安全存储、选中真实生效;联合开发默认关闭,beta 不含 npm",
|
|
12
|
+
"result": {
|
|
13
|
+
"outcome": "ok",
|
|
14
|
+
"assertions": {
|
|
15
|
+
"s0_token": "w2m-operator set/status/clear, 0600 file, fingerprint only (v1.9.15)",
|
|
16
|
+
"s0_capabilities": "relay /healthz relay_version/features; w2m_status exposes them; negotiation fail-closed (v1.9.16)",
|
|
17
|
+
"s1_selection": "pre-dispatch TARGET_UNAVAILABLE; post-dispatch SELECTION_NOT_HONOURED with cancel (v1.9.12)",
|
|
18
|
+
"joint_cli": "w2m-joint plan/approve/status/dispatch --dry-run with capability negotiation (v1.9.13 + current)",
|
|
19
|
+
"scrub_cli": "w2m-scrub recursive blobs, dry-run/apply, external backup, idempotent (current)",
|
|
20
|
+
"open": "G0-3 relay upgrade/cleanup needs SSH window; G0-4 machine version alignment needs operator token; enforce remains off"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "2.0.0",
|
|
3
|
+
"case": "G0-3-106.53.197.47",
|
|
4
|
+
"kind": "positive",
|
|
5
|
+
"executed_at": "2026-10-28T03:50:51.939Z",
|
|
6
|
+
"executor": "lead",
|
|
7
|
+
"task_id": "g0-3-106.53.197.47-1793159451940",
|
|
8
|
+
"requirement": "relay 升级到 ≥1.9.16,广告 relay_version/features,持久面数据面对象为 0",
|
|
9
|
+
"result": {
|
|
10
|
+
"outcome": "ok",
|
|
11
|
+
"assertions": {
|
|
12
|
+
"method": "SSH (w2m-fleet-deploy) + tarball layout /opt/w2m/app, npm registry tgz sha256 verified",
|
|
13
|
+
"before": {
|
|
14
|
+
"relay_version": "1.9.12",
|
|
15
|
+
"features": null,
|
|
16
|
+
"blobs": 0
|
|
17
|
+
},
|
|
18
|
+
"after": {
|
|
19
|
+
"relay_version": "1.9.16",
|
|
20
|
+
"features": [
|
|
21
|
+
"target-machines",
|
|
22
|
+
"result-projection",
|
|
23
|
+
"guard-v1",
|
|
24
|
+
"relay-version-v1"
|
|
25
|
+
],
|
|
26
|
+
"blobs": 0,
|
|
27
|
+
"blob_bytes": 0
|
|
28
|
+
},
|
|
29
|
+
"relay_id": "de5d1842d2d28df9",
|
|
30
|
+
"healthz": "http://106.53.197.47:8787/healthz",
|
|
31
|
+
"backup": "/opt/w2m/app.bak-* and /root/w2m-upgrade/<ts>/state on the host",
|
|
32
|
+
"rollback": "script trap restores previous app dir and service; not triggered"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "2.0.0",
|
|
3
|
+
"case": "G0-3-202.182.123.154",
|
|
4
|
+
"kind": "positive",
|
|
5
|
+
"executed_at": "2026-10-28T03:50:51.939Z",
|
|
6
|
+
"executor": "lead",
|
|
7
|
+
"task_id": "g0-3-202.182.123.154-1793159451941",
|
|
8
|
+
"requirement": "relay 升级到 ≥1.9.16,广告 relay_version/features,持久面数据面对象为 0",
|
|
9
|
+
"result": {
|
|
10
|
+
"outcome": "ok",
|
|
11
|
+
"assertions": {
|
|
12
|
+
"method": "SSH (w2m-fleet-deploy) + tarball layout /opt/w2m/app, npm registry tgz sha256 verified",
|
|
13
|
+
"before": {
|
|
14
|
+
"relay_version": "1.9.12",
|
|
15
|
+
"features": null,
|
|
16
|
+
"blobs": 0
|
|
17
|
+
},
|
|
18
|
+
"after": {
|
|
19
|
+
"relay_version": "1.9.16",
|
|
20
|
+
"features": [
|
|
21
|
+
"target-machines",
|
|
22
|
+
"result-projection",
|
|
23
|
+
"guard-v1",
|
|
24
|
+
"relay-version-v1"
|
|
25
|
+
],
|
|
26
|
+
"blobs": 0,
|
|
27
|
+
"blob_bytes": 0
|
|
28
|
+
},
|
|
29
|
+
"relay_id": "595a29b256890689",
|
|
30
|
+
"healthz": "http://202.182.123.154:8787/healthz",
|
|
31
|
+
"backup": "/opt/w2m/app.bak-* and /root/w2m-upgrade/<ts>/state on the host",
|
|
32
|
+
"rollback": "script trap restores previous app dir and service; not triggered"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "2.0.0",
|
|
3
|
+
"case": "G0-3-43.142.74.71",
|
|
4
|
+
"kind": "positive",
|
|
5
|
+
"executed_at": "2026-10-28T03:50:51.939Z",
|
|
6
|
+
"executor": "lead",
|
|
7
|
+
"task_id": "g0-3-43.142.74.71-1793159451940",
|
|
8
|
+
"requirement": "relay 升级到 ≥1.9.16,广告 relay_version/features,持久面数据面对象为 0",
|
|
9
|
+
"result": {
|
|
10
|
+
"outcome": "ok",
|
|
11
|
+
"assertions": {
|
|
12
|
+
"method": "SSH (w2m-fleet-deploy) + tarball layout /opt/w2m/app, npm registry tgz sha256 verified",
|
|
13
|
+
"before": {
|
|
14
|
+
"relay_version": "1.9.12",
|
|
15
|
+
"features": null,
|
|
16
|
+
"blobs": 0
|
|
17
|
+
},
|
|
18
|
+
"after": {
|
|
19
|
+
"relay_version": "1.9.16",
|
|
20
|
+
"features": [
|
|
21
|
+
"target-machines",
|
|
22
|
+
"result-projection",
|
|
23
|
+
"guard-v1",
|
|
24
|
+
"relay-version-v1"
|
|
25
|
+
],
|
|
26
|
+
"blobs": 0,
|
|
27
|
+
"blob_bytes": 0
|
|
28
|
+
},
|
|
29
|
+
"relay_id": "e34908005df80fe3",
|
|
30
|
+
"healthz": "http://43.142.74.71:8787/healthz",
|
|
31
|
+
"backup": "/opt/w2m/app.bak-* and /root/w2m-upgrade/<ts>/state on the host",
|
|
32
|
+
"rollback": "script trap restores previous app dir and service; not triggered"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "2.0.0",
|
|
3
|
+
"case": "R1-OPERATOR-TOKEN",
|
|
4
|
+
"kind": "positive",
|
|
5
|
+
"executed_at": "2026-10-28T03:50:51.939Z",
|
|
6
|
+
"executor": "lead",
|
|
7
|
+
"task_id": "r1-token-1793159451943",
|
|
8
|
+
"requirement": "operator token 通过 0600 文件安全写入,状态只显示指纹",
|
|
9
|
+
"result": {
|
|
10
|
+
"outcome": "ok",
|
|
11
|
+
"assertions": {
|
|
12
|
+
"path": "C:/Users/fangw/.w2m/operator-token",
|
|
13
|
+
"source": "file",
|
|
14
|
+
"fingerprint": "209e400801c6",
|
|
15
|
+
"value_handling": "token read from relay unit/state by the operator, written via --file, temp file deleted; never printed",
|
|
16
|
+
"note": "running DSH process must be restarted to pick the file into w2m_run"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "2.0.0",
|
|
3
|
+
"case": "R3-CAPABILITIES",
|
|
4
|
+
"kind": "positive",
|
|
5
|
+
"executed_at": "2026-10-28T03:50:51.939Z",
|
|
6
|
+
"executor": "lead",
|
|
7
|
+
"task_id": "r3-capabilities-1793159451943",
|
|
8
|
+
"requirement": "三台 relay 广告 relay_version/features;客户端能力协商通过;旧 relay 仍 fail-closed",
|
|
9
|
+
"result": {
|
|
10
|
+
"outcome": "ok",
|
|
11
|
+
"assertions": {
|
|
12
|
+
"relays": [
|
|
13
|
+
{
|
|
14
|
+
"host": "43.142.74.71",
|
|
15
|
+
"relay_version": "1.9.16",
|
|
16
|
+
"features": [
|
|
17
|
+
"target-machines",
|
|
18
|
+
"result-projection",
|
|
19
|
+
"guard-v1",
|
|
20
|
+
"relay-version-v1"
|
|
21
|
+
],
|
|
22
|
+
"blobs": 0
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"host": "106.53.197.47",
|
|
26
|
+
"relay_version": "1.9.16",
|
|
27
|
+
"features": [
|
|
28
|
+
"target-machines",
|
|
29
|
+
"result-projection",
|
|
30
|
+
"guard-v1",
|
|
31
|
+
"relay-version-v1"
|
|
32
|
+
],
|
|
33
|
+
"blobs": 0
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"host": "202.182.123.154",
|
|
37
|
+
"relay_version": "1.9.16",
|
|
38
|
+
"features": [
|
|
39
|
+
"target-machines",
|
|
40
|
+
"result-projection",
|
|
41
|
+
"guard-v1",
|
|
42
|
+
"relay-version-v1"
|
|
43
|
+
],
|
|
44
|
+
"blobs": 0
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"client_negotiation": "w2m-joint dispatch --dry-run --relay http://43.142.74.71:8787 -> relay_negotiation.ok=true",
|
|
48
|
+
"fail_closed": "old relay without features still returns RELAY_CAPABILITIES_MISSING (unit test)"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "2.0.0",
|
|
3
|
+
"case": "R8-LEDGER-JOURNAL",
|
|
4
|
+
"kind": "positive",
|
|
5
|
+
"executed_at": "2026-10-28T03:55:45.117Z",
|
|
6
|
+
"executor": "lead",
|
|
7
|
+
"task_id": "r8-ledger-journal-1793159745118",
|
|
8
|
+
"requirement": "relay ledger 只存 command_hash/command_argv_len;argv 移到独立 offer journal;重启从 journal 恢复;command_hash 由 relay 派生",
|
|
9
|
+
"result": {
|
|
10
|
+
"outcome": "ok",
|
|
11
|
+
"assertions": {
|
|
12
|
+
"ledger": "task.created 行不含 \"command_argv\",含 command_argv_len(e2e 断言)",
|
|
13
|
+
"journal": "<stateDir>/offers/<task_id>.json 0600,字段 argv/stages/reduce_argv,写入先于任务通告",
|
|
14
|
+
"restart": "同一 stateDir 重启后 getTask(task_id).command_argv 从 journal 恢复,且 offer_unavailable 不为 true",
|
|
15
|
+
"hash": "调用方传入不匹配的 command_hash -> 400 BAD_REQUEST(relay 自行派生)",
|
|
16
|
+
"journal_lifecycle": "cancelled 删除;启动时 prune 掉无任务的 journal",
|
|
17
|
+
"tests": "test/relay-offer-journal.test.mjs 3/3;relay/joint/recovery/p2p 22 套件 384 项 383 pass / 1 skip / 0 fail;e2e 11/11",
|
|
18
|
+
"known_limit": "journal 不是数据面对象,但仍含指令 argv(控制面允许);扫描器按字段名不命中,需文档声明其归属"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|