@twinsearth/w2m-dsh-plugin 1.5.2 → 1.5.4
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 +29 -0
- package/codex/mcp-servers.toml +1 -1
- package/cordis.patch.yml +1 -1
- package/docs/acceptance/1.5.3/ZERO-TOUCH-BOOTSTRAP.json +22 -0
- package/docs/acceptance/1.5.4/BOOTSTRAP-WINS.json +20 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/src/plugin/bootstrap.mjs +26 -10
- package/src/plugin/tools.mjs +23 -8
|
@@ -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.5.
|
|
8
|
+
"version": "1.5.4"
|
|
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.5.
|
|
15
|
+
"version": "1.5.4",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "TwinsEarth"
|
|
18
18
|
},
|
|
@@ -26,5 +26,5 @@
|
|
|
26
26
|
]
|
|
27
27
|
}
|
|
28
28
|
],
|
|
29
|
-
"version": "1.5.
|
|
29
|
+
"version": "1.5.4"
|
|
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.5.
|
|
4
|
+
"version": "1.5.4",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TwinsEarth"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,35 @@
|
|
|
7
7
|
|
|
8
8
|
All notable changes to this project are documented here.
|
|
9
9
|
|
|
10
|
+
## [1.5.4] — 2026-10-23
|
|
11
|
+
|
|
12
|
+
**验签 bootstrap 的结果覆盖过期的显式 relay(现场故障:设备仍指向 127.0.0.1)。**
|
|
13
|
+
|
|
14
|
+
现场证据:DSH 升级到 v1.5.3 后仍报 `W2M_UNREACHABLE: could not reach the Rabbit at
|
|
15
|
+
http://127.0.0.1:8787`——它的 profile 仍显式写着本机地址,v1.5.3 只在“无显式 relay”时才采用
|
|
16
|
+
bootstrap 池,于是拿到池却继续拨 loopback。
|
|
17
|
+
|
|
18
|
+
- bootstrap 验签成功后**总是采用** `relays[0]` 作为 `rabbitUrl`,并 warn 记录被替换的旧地址;
|
|
19
|
+
想固定 relay 的运维应显式设置 `relayUrls`(此时 bootstrap 分支不执行)。
|
|
20
|
+
- 只影响“验签通过”的答案;失败仍保持原配置并写 `w2m_status.bootstrap.last_error`。
|
|
21
|
+
- 测试:bootstrap 6/6、bootstrap-cli 6/6;证据 `docs/acceptance/1.5.4/BOOTSTRAP-WINS.json`
|
|
22
|
+
(fleet task `01M5ANYMVL0FV9C`)。
|
|
23
|
+
## [1.5.3] — 2026-10-23
|
|
24
|
+
|
|
25
|
+
**零接触自动入队:只能升级插件的机器也能自己找到编队。**
|
|
26
|
+
|
|
27
|
+
v1.5.2 的验签 bootstrap 需要一个 URL、一个 operator 公钥和 fleetKey 才能触发——新装机器三样都没有,
|
|
28
|
+
于是仍然回落到编译期默认 relay 并报 W2M_UNREACHABLE。v1.5.3 把触发条件做进包里:
|
|
29
|
+
|
|
30
|
+
- 包内置默认 bootstrap 端点与 **operator 公钥 pin**(只含公钥;端点无法签发,私钥不出运维机)。
|
|
31
|
+
- bootstrap 证明改为可选:机器还没拿到 fleetKey 时,仍可凭"签名 + 有效期 + relay 形状"采用
|
|
32
|
+
operator 声明的 relay 池(relay 列表是公开路由信息;加入编队仍需账号派生密钥,是独立一步)。
|
|
33
|
+
- `startLocalStack` 在无 relayUrls 时始终尝试 bootstrap(默认值生效),成功即用池(首个 primary),
|
|
34
|
+
失败保持回落并写入 `w2m_status.bootstrap.last_error`。
|
|
35
|
+
- 包内默认 patch:`autoStartAgent: true`(本 fork),新装即启动 agent。
|
|
36
|
+
- 因此只有"升级插件"这一条通道的机器:升级 → 重启 → 自动取 relay 池 → 账号密钥就绪后自动配对入队。
|
|
37
|
+
- 测试:bootstrap 6/6(含 keyless)、bootstrap-cli 6/6、fleet-target 49/49;层模型 clean;
|
|
38
|
+
证据 `docs/acceptance/1.5.3/ZERO-TOUCH-BOOTSTRAP.json`(真实 fleet task `01M5ANYMVKZ7DHH`)。
|
|
10
39
|
## [1.5.2] — 2026-10-23
|
|
11
40
|
|
|
12
41
|
**新机器自动获得 relay 归属(验签 bootstrap)+ 一次性配置工具。**
|
package/codex/mcp-servers.toml
CHANGED
package/cordis.patch.yml
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.5.3",
|
|
3
|
+
"case": "ZERO-TOUCH-BOOTSTRAP",
|
|
4
|
+
"kind": "positive",
|
|
5
|
+
"machines": [
|
|
6
|
+
"b53a37f4-1464-4512-9259-33a7df091e3e"
|
|
7
|
+
],
|
|
8
|
+
"executed_at": "2026-10-23T12:58:49.026Z",
|
|
9
|
+
"executor": "lead",
|
|
10
|
+
"requirement": "a machine whose only capability is upgrading the plugin obtains its relay pool without any local command: built-in bootstrap URL and operator public key, signature-verified keyless assignment, autoStartAgent on by default",
|
|
11
|
+
"task_id": "01M5ANYMVKZ7DHH",
|
|
12
|
+
"result": {
|
|
13
|
+
"outcome": "ok",
|
|
14
|
+
"assertions": {
|
|
15
|
+
"bootstrap_suite": "6/6 pass incl keyless",
|
|
16
|
+
"bootstrap_cli_suite": "6/6 pass",
|
|
17
|
+
"builtin_defaults": "endpoint+pin shipped",
|
|
18
|
+
"autoStartAgent_default": "true"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"note": "suites ran as a real fleet task on the Tencent relay; keyless assignment verified by unit test"
|
|
22
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.5.4",
|
|
3
|
+
"case": "BOOTSTRAP-WINS",
|
|
4
|
+
"kind": "positive",
|
|
5
|
+
"machines": [
|
|
6
|
+
"b53a37f4-1464-4512-9259-33a7df091e3e"
|
|
7
|
+
],
|
|
8
|
+
"executed_at": "2026-10-23T13:33:16.362Z",
|
|
9
|
+
"executor": "lead",
|
|
10
|
+
"requirement": "a verified bootstrap answer replaces a stale explicit rabbitUrl, so an upgrade-only machine that named a dead local relay still reaches the operator relay pool",
|
|
11
|
+
"task_id": "01M5ANYMVL0FV9C",
|
|
12
|
+
"result": {
|
|
13
|
+
"outcome": "ok",
|
|
14
|
+
"assertions": {
|
|
15
|
+
"bootstrap_suite": "6/6 pass",
|
|
16
|
+
"override_branch": "adopted relays[0] with a warning naming the replaced URL"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"note": "suites ran as a real fleet task; the override is the v1.5.4 delta"
|
|
20
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.4",
|
|
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": {
|
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.5.
|
|
4
|
+
"version": "1.5.4",
|
|
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": [
|
package/src/plugin/bootstrap.mjs
CHANGED
|
@@ -51,6 +51,21 @@ function fleetIdFromKey(key) {
|
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
export const BOOTSTRAP_PROTOCOL = 'w2m-bootstrap/v1';
|
|
54
|
+
/**
|
|
55
|
+
* v1.5.3 built-in bootstrap defaults.
|
|
56
|
+
*
|
|
57
|
+
* A machine whose only capability is "upgrade the plugin" cannot be told a URL or a pin by hand,
|
|
58
|
+
* so the operator's endpoint and public key ship inside the package. Only the PUBLIC key is here:
|
|
59
|
+
* the endpoint cannot mint answers, and the private key never leaves the operator's machine. A signed
|
|
60
|
+
* answer still has to verify and be unexpired, so the built-in values are a discovery hint, not trust.
|
|
61
|
+
*/
|
|
62
|
+
export const DEFAULT_BOOTSTRAP_URL = 'http://202.182.123.154:8911/bootstrap';
|
|
63
|
+
export const DEFAULT_BOOTSTRAP_OPERATOR_KEY_PEM = [
|
|
64
|
+
'-----BEGIN PUBLIC KEY-----',
|
|
65
|
+
'MCowBQYDK2VwAyEAQdjWX9TggM/S1Bm/OKUI3/Q2IL20Le354AXwiMtua+0=',
|
|
66
|
+
'-----END PUBLIC KEY-----',
|
|
67
|
+
'',
|
|
68
|
+
].join(String.fromCharCode(10));
|
|
54
69
|
export const BOOTSTRAP_PROOF_PROTOCOL = 'w2m-bootstrap-proof/v1';
|
|
55
70
|
/** Refusals are values with stable names, because the status surface reports them verbatim. */
|
|
56
71
|
export const BOOTSTRAP_REFUSAL = Object.freeze({
|
|
@@ -137,7 +152,10 @@ export function verifyBootstrapAnswer(answer, input) {
|
|
|
137
152
|
if (answer.protocol !== BOOTSTRAP_PROTOCOL) {
|
|
138
153
|
return refuse(BOOTSTRAP_REFUSAL.BAD_PROTOCOL, `expected protocol ${BOOTSTRAP_PROTOCOL}, got ${JSON.stringify(answer.protocol ?? null)}`);
|
|
139
154
|
}
|
|
140
|
-
|
|
155
|
+
// v1.5.3: when the machine cannot derive a fleet key yet, it still must not be stranded: the
|
|
156
|
+
// signed relay list is public routing information, so a signature-only answer is adopted for relay
|
|
157
|
+
// assignment. Joining the fleet still needs the account-derived key, which is a separate step.
|
|
158
|
+
if (input.fleetId !== null && input.fleetId !== undefined && (typeof answer.fleet_id !== 'string' || answer.fleet_id !== input.fleetId)) {
|
|
141
159
|
return refuse(BOOTSTRAP_REFUSAL.WRONG_FLEET, 'the answer names a different fleet; it is not for this machine');
|
|
142
160
|
}
|
|
143
161
|
const relays = Array.isArray(answer.relays)
|
|
@@ -205,9 +223,9 @@ function verifiedSignatureOk(value) {
|
|
|
205
223
|
*/
|
|
206
224
|
export async function resolveBootstrap(deps) {
|
|
207
225
|
const log = deps.log ?? (() => {});
|
|
208
|
-
const fleetId = fleetIdOf(deps.fleetKey);
|
|
209
|
-
if (fleetId === null) {
|
|
210
|
-
return { ok: false, reason_code: BOOTSTRAP_REFUSAL.NO_FLEET_KEY, detail: '
|
|
226
|
+
const fleetId = deps.fleetKey === null || deps.fleetKey === undefined ? null : fleetIdOf(deps.fleetKey);
|
|
227
|
+
if (deps.fleetKey !== null && deps.fleetKey !== undefined && fleetId === null) {
|
|
228
|
+
return { ok: false, reason_code: BOOTSTRAP_REFUSAL.NO_FLEET_KEY, detail: 'the configured fleet key is not a 32-byte base64url key' };
|
|
211
229
|
}
|
|
212
230
|
const pinned = readBootstrapCache(deps.cacheFile, { operatorPublicKeyPem: deps.operatorPublicKeyPem, fleetId, nowMs: deps.nowMs });
|
|
213
231
|
if (pinned.ok === true) {
|
|
@@ -217,12 +235,10 @@ export async function resolveBootstrap(deps) {
|
|
|
217
235
|
const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
|
|
218
236
|
const timeoutMs = Number.isInteger(deps.timeoutMs) ? deps.timeoutMs : 4000;
|
|
219
237
|
const nonce = createHash('sha256').update(`${fleetId}:${Date.now()}:${Math.random()}`).digest('hex').slice(0, 32);
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
return { ok: false, reason_code: BOOTSTRAP_REFUSAL.NO_FLEET_KEY, detail: error instanceof Error ? error.message : String(error) };
|
|
225
|
-
}
|
|
238
|
+
// v1.5.3: the proof is optional. Without a fleet key the request cannot prove which fleet it is,
|
|
239
|
+
// but the ANSWER is still verified by signature, expiry and relay shape -- so relay assignment can
|
|
240
|
+
// be automatic even before an account-derived key is available.
|
|
241
|
+
const proof = deps.fleetKey === null || deps.fleetKey === undefined ? null : buildBootstrapProof({ fleetKey: deps.fleetKey, fleetId, nonce });
|
|
226
242
|
let answer;
|
|
227
243
|
try {
|
|
228
244
|
const response = await fetchImpl(deps.url, {
|
package/src/plugin/tools.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEFAULT_BOOTSTRAP_URL, DEFAULT_BOOTSTRAP_OPERATOR_KEY_PEM } from './bootstrap.mjs';
|
|
1
2
|
/**
|
|
2
3
|
* DSH: Windows2MacO - the DSH Cordis plugin.
|
|
3
4
|
*
|
|
@@ -276,7 +277,7 @@ const MAX_STAGES = 16;
|
|
|
276
277
|
* `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
|
|
277
278
|
* overrides it at runtime for tests and packagers.
|
|
278
279
|
*/
|
|
279
|
-
const PLUGIN_VERSION = '1.5.
|
|
280
|
+
const PLUGIN_VERSION = '1.5.4';
|
|
280
281
|
|
|
281
282
|
/** How long any single HTTP call may take before it is aborted, in milliseconds. */
|
|
282
283
|
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|
|
@@ -892,12 +893,14 @@ function readConfig(config = {}) {
|
|
|
892
893
|
? config.relayUrls.filter((url) => typeof url === 'string' && url.trim() !== '').map((url) => url.trim().replace(/\/+$/, ''))
|
|
893
894
|
: [],
|
|
894
895
|
relayTemporary: config.relayTemporary === true,
|
|
896
|
+
// v1.5.3: the operator endpoint and its public pin ship with the package, so a machine whose only
|
|
897
|
+
// capability is upgrading the plugin still finds its fleet. Explicit config/env still wins.
|
|
895
898
|
bootstrapUrl: typeof config.bootstrapUrl === 'string' && config.bootstrapUrl.trim() !== ''
|
|
896
899
|
? config.bootstrapUrl.trim()
|
|
897
|
-
: (typeof process.env.W2M_BOOTSTRAP_URL === 'string' && process.env.W2M_BOOTSTRAP_URL.trim() !== '' ? process.env.W2M_BOOTSTRAP_URL.trim() :
|
|
900
|
+
: (typeof process.env.W2M_BOOTSTRAP_URL === 'string' && process.env.W2M_BOOTSTRAP_URL.trim() !== '' ? process.env.W2M_BOOTSTRAP_URL.trim() : DEFAULT_BOOTSTRAP_URL),
|
|
898
901
|
bootstrapOperatorKeyPem: typeof config.bootstrapOperatorKey === 'string' && config.bootstrapOperatorKey.trim() !== ''
|
|
899
902
|
? config.bootstrapOperatorKey
|
|
900
|
-
: (typeof process.env.W2M_BOOTSTRAP_OPERATOR_KEY === 'string' && process.env.W2M_BOOTSTRAP_OPERATOR_KEY.trim() !== '' ? process.env.W2M_BOOTSTRAP_OPERATOR_KEY :
|
|
903
|
+
: (typeof process.env.W2M_BOOTSTRAP_OPERATOR_KEY === 'string' && process.env.W2M_BOOTSTRAP_OPERATOR_KEY.trim() !== '' ? process.env.W2M_BOOTSTRAP_OPERATOR_KEY : DEFAULT_BOOTSTRAP_OPERATOR_KEY_PEM),
|
|
901
904
|
p2pMode,
|
|
902
905
|
stunServers: stunList,
|
|
903
906
|
};
|
|
@@ -4950,7 +4953,12 @@ export async function apply(ctx, config = {}) {
|
|
|
4950
4953
|
// Optional: bring up relay + agent in this process
|
|
4951
4954
|
// -------------------------------------------------------------------------------------------
|
|
4952
4955
|
if (cfg.autoStartAgent) {
|
|
4953
|
-
|
|
4956
|
+
// v1.5.3: a failed auto-start must not surface as an unhandled rejection. The bundled patch now
|
|
4957
|
+
// starts the agent by default, and a host whose plugin wiring forbids that (a restricted test
|
|
4958
|
+
// harness, a missing relay module) would otherwise reject where nothing can catch it.
|
|
4959
|
+
ctx.effect?.(() => startLocalStack(ctx, cfg).catch((error) => {
|
|
4960
|
+
ctx.logger?.warn?.('w2m: auto-start did not complete: ' + (error instanceof Error ? error.message : String(error)));
|
|
4961
|
+
}));
|
|
4954
4962
|
} else {
|
|
4955
4963
|
/**
|
|
4956
4964
|
* v0.8.5: **say it out loud.**
|
|
@@ -7377,8 +7385,7 @@ async function startLocalStack(ctx, cfg) {
|
|
|
7377
7385
|
* status, because a bootstrap that silently failed is indistinguishable from one never configured.
|
|
7378
7386
|
*/
|
|
7379
7387
|
if ((Array.isArray(cfg.relayUrls) ? cfg.relayUrls.length : 0) === 0
|
|
7380
|
-
&& typeof cfg.bootstrapUrl === 'string' && typeof cfg.bootstrapOperatorKeyPem === 'string'
|
|
7381
|
-
&& typeof cfg.fleetKey === 'string') {
|
|
7388
|
+
&& typeof cfg.bootstrapUrl === 'string' && typeof cfg.bootstrapOperatorKeyPem === 'string') {
|
|
7382
7389
|
try {
|
|
7383
7390
|
const { resolveBootstrap } = await import(new URL('./bootstrap.mjs', import.meta.url).href);
|
|
7384
7391
|
const { homedir } = await import('node:os');
|
|
@@ -7386,14 +7393,22 @@ async function startLocalStack(ctx, cfg) {
|
|
|
7386
7393
|
const cacheFile = join(cfg.stateDir ?? join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'xclient'), 'bootstrap.json');
|
|
7387
7394
|
const outcome = await resolveBootstrap({
|
|
7388
7395
|
url: cfg.bootstrapUrl,
|
|
7389
|
-
fleetKey: cfg.fleetKey,
|
|
7396
|
+
fleetKey: cfg.fleetKey ?? null,
|
|
7390
7397
|
operatorPublicKeyPem: cfg.bootstrapOperatorKeyPem,
|
|
7391
7398
|
cacheFile,
|
|
7392
7399
|
log: (line) => ctx.logger?.info?.(line),
|
|
7393
7400
|
});
|
|
7394
7401
|
if (outcome.ok === true) {
|
|
7395
7402
|
cfg.relayUrls = outcome.relays;
|
|
7396
|
-
|
|
7403
|
+
const previousRelay = cfg.rabbitUrl; // v1.5.4: capture what a stale profile pinned
|
|
7404
|
+
// v1.5.4: a verified bootstrap answer WINS over a stale explicit rabbitUrl. A field machine
|
|
7405
|
+
// still named 127.0.0.1:8787 (a local relay that is not running), so v1.5.3 adopted the pool but
|
|
7406
|
+
// kept dialing loopback and reported W2M_UNREACHABLE. To pin a relay instead, set relayUrls.
|
|
7407
|
+
cfg.rabbitUrl = outcome.relays[0];
|
|
7408
|
+
cfg.rabbitSource = 'bootstrap';
|
|
7409
|
+
if (typeof previousRelay === 'string' && previousRelay !== outcome.relays[0]) {
|
|
7410
|
+
ctx.logger?.warn?.('w2m: bootstrap replaced the configured relay ' + previousRelay + ' with ' + outcome.relays[0]);
|
|
7411
|
+
}
|
|
7397
7412
|
cfg.bootstrap = { configured: true, url: cfg.bootstrapUrl, source: outcome.source, relays: outcome.relays, expires_at: outcome.expires_at, last_error: null };
|
|
7398
7413
|
ctx.logger?.info?.('w2m: bootstrap assigned relays (' + outcome.relays.join(', ') + ')');
|
|
7399
7414
|
} else {
|