@twinsearth/w2m-dsh-plugin 1.9.16 → 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 +15 -0
- package/bin/w2m-joint.mjs +36 -5
- package/bin/w2m-scrub.mjs +78 -0
- package/codex/mcp-servers.toml +1 -1
- 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/tools.mjs +5 -3
- 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/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,21 @@
|
|
|
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
|
+
|
|
10
25
|
## [1.9.16] - 2026-10-28
|
|
11
26
|
|
|
12
27
|
- G0-2:relay /healthz 新增 relay_version 与 features(target-machines / result-projection / guard-v1 / relay-version-v1);w2m_status 显示这两项,便于核对三台 relay 的实际版本与能力。
|
package/bin/w2m-joint.mjs
CHANGED
|
@@ -21,9 +21,13 @@ import { readFileSync } from 'node:fs';
|
|
|
21
21
|
|
|
22
22
|
import { buildControlEnvelopes, buildPlan } from '../src/plugin/joint-dev/dispatch.mjs';
|
|
23
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';
|
|
24
27
|
import {
|
|
25
28
|
approvePlan,
|
|
26
29
|
assertDispatchAllowed,
|
|
30
|
+
consumeApproval,
|
|
27
31
|
listPlans,
|
|
28
32
|
loadApproval,
|
|
29
33
|
loadPlan,
|
|
@@ -55,7 +59,7 @@ if (stateDir === null) fail('STATE_DIR_REQUIRED', 'pass --state <dir> (the joint
|
|
|
55
59
|
if (command === 'plan' && action === 'create') {
|
|
56
60
|
if (typeof args.steps !== 'string') fail('STEPS_REQUIRED', 'pass --steps <steps.json>');
|
|
57
61
|
let steps;
|
|
58
|
-
try { steps = JSON.parse(readFileSync(args.steps, 'utf8').replace(
|
|
62
|
+
try { steps = JSON.parse(readFileSync(args.steps, 'utf8').replace(/^\uFEFF/, '')); } catch (error) {
|
|
59
63
|
fail('STEPS_UNREADABLE', error instanceof Error ? error.message : String(error));
|
|
60
64
|
}
|
|
61
65
|
if (!Array.isArray(steps)) fail('STEPS_INVALID', 'steps.json must be a JSON array');
|
|
@@ -99,6 +103,10 @@ if (command === 'approve') {
|
|
|
99
103
|
const authority = Number.parseInt(String(args.authority ?? ''), 10);
|
|
100
104
|
if (!Number.isInteger(authority)) fail('AUTHORITY_REQUIRED', 'pass --authority <0..100>');
|
|
101
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);
|
|
102
110
|
const approved = approvePlan({
|
|
103
111
|
stateDir,
|
|
104
112
|
planSha: args.plan,
|
|
@@ -106,9 +114,14 @@ if (command === 'approve') {
|
|
|
106
114
|
authority,
|
|
107
115
|
note: typeof args.note === 'string' ? args.note : '',
|
|
108
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
|
+
}),
|
|
109
122
|
});
|
|
110
123
|
if (approved.ok !== true) fail(approved.code ?? 'APPROVAL_FAILED', approved.detail ?? null);
|
|
111
|
-
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 });
|
|
112
125
|
process.exit(0);
|
|
113
126
|
}
|
|
114
127
|
|
|
@@ -117,12 +130,13 @@ if (command === 'status') {
|
|
|
117
130
|
const plan = loadPlan({ stateDir, planSha: args.plan });
|
|
118
131
|
if (plan.ok !== true) fail(plan.code ?? 'PLAN_NOT_FOUND');
|
|
119
132
|
const approval = loadApproval({ stateDir, planSha: args.plan });
|
|
120
|
-
const gate = assertDispatchAllowed({ stateDir, planSha: args.plan });
|
|
133
|
+
const gate = assertDispatchAllowed({ stateDir, planSha: args.plan, requireSignature: args['allow-unsigned-approval'] !== true });
|
|
121
134
|
print({
|
|
122
135
|
ok: true,
|
|
123
136
|
plan_sha256: plan.plan.plan_sha256,
|
|
124
137
|
steps: plan.plan.steps.map((step) => ({ step_id: step.step_id, machine_id: step.machine_id, command_hash: step.command_hash })),
|
|
125
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,
|
|
126
140
|
dispatch_allowed: gate.ok === true,
|
|
127
141
|
blocked_by: gate.ok === true ? null : gate.code,
|
|
128
142
|
});
|
|
@@ -131,10 +145,27 @@ if (command === 'status') {
|
|
|
131
145
|
|
|
132
146
|
if (command === 'dispatch') {
|
|
133
147
|
if (typeof args.plan !== 'string') fail('PLAN_REQUIRED', 'pass --plan <sha256>');
|
|
134
|
-
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 });
|
|
135
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;
|
|
136
153
|
if (args['dry-run'] !== true) {
|
|
137
|
-
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);
|
|
138
169
|
}
|
|
139
170
|
let capabilities = null;
|
|
140
171
|
let negotiation = null;
|
|
@@ -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,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
|
+
}
|