@twinsearth/w2m-dsh-plugin 1.9.15 → 1.9.16
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 +6 -0
- package/bin/w2m-joint.mjs +13 -0
- package/codex/mcp-servers.toml +1 -1
- package/docs/acceptance/1.9.16/RELEASE-1916.json +21 -0
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/src/plugin/relay-capabilities.mjs +66 -0
- package/src/plugin/tools.mjs +5 -3
- package/src/relay/server.mjs +14 -1
|
@@ -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.16"
|
|
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.16",
|
|
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.16"
|
|
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.16",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TwinsEarth"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
|
|
8
8
|
All notable changes to this project are documented here.
|
|
9
9
|
|
|
10
|
+
## [1.9.16] - 2026-10-28
|
|
11
|
+
|
|
12
|
+
- G0-2:relay /healthz 新增 relay_version 与 features(target-machines / result-projection / guard-v1 / relay-version-v1);w2m_status 显示这两项,便于核对三台 relay 的实际版本与能力。
|
|
13
|
+
- 新增 relay-capabilities 模块与 w2m-joint dispatch 能力协商:目标 relay 缺少必需 feature(或旧 relay 完全不广告)时,联合派发 fail-closed 返回 RELAY_CAPABILITIES_MISSING,不靠“先发再发现”。
|
|
14
|
+
- 新增 test/relay-health-capabilities.test.mjs(5 项)。
|
|
15
|
+
|
|
10
16
|
## [1.9.15] - 2026-10-28
|
|
11
17
|
|
|
12
18
|
- 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,6 +20,7 @@
|
|
|
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';
|
|
23
24
|
import {
|
|
24
25
|
approvePlan,
|
|
25
26
|
assertDispatchAllowed,
|
|
@@ -135,10 +136,22 @@ if (command === 'dispatch') {
|
|
|
135
136
|
if (args['dry-run'] !== true) {
|
|
136
137
|
fail('DISPATCH_NOT_WIRED', 'real dispatch is the next slice: it needs the operator token and a relay with target_machines/capability advertisement; use --dry-run to inspect the envelopes');
|
|
137
138
|
}
|
|
139
|
+
let capabilities = null;
|
|
140
|
+
let negotiation = null;
|
|
141
|
+
if (typeof args.relay === 'string') {
|
|
142
|
+
capabilities = await readRelayCapabilities({ rabbitUrl: args.relay });
|
|
143
|
+
negotiation = negotiateFeatures(capabilities);
|
|
144
|
+
if (negotiation.ok !== true) {
|
|
145
|
+
fail(negotiation.code ?? 'RELAY_CAPABILITIES_MISSING', { missing: negotiation.missing ?? null, relay: args.relay });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
138
148
|
const envelopes = buildControlEnvelopes(gate.plan);
|
|
139
149
|
print({
|
|
140
150
|
ok: true,
|
|
141
151
|
dry_run: true,
|
|
152
|
+
relay: typeof args.relay === 'string' ? args.relay : null,
|
|
153
|
+
relay_capabilities: capabilities === null ? null : { relay_version: capabilities.relay_version, features: capabilities.features },
|
|
154
|
+
relay_negotiation: negotiation,
|
|
142
155
|
plan_sha256: gate.plan.plan_sha256,
|
|
143
156
|
approval_sha256: gate.approval.approval_sha256,
|
|
144
157
|
envelopes: envelopes.length,
|
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.16",
|
|
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 test/grant-issue.test.mjs test/project-refs.test.mjs test/exec-capability.test.mjs test/secondary-link.test.mjs test/bootstrap-cli.test.mjs test/bootstrap.test.mjs test/windows-hidden-task.test.mjs test/relay-failover.test.mjs test/npm-update.test.mjs test/agent-start-nonblocking.test.mjs test/fleet-chat-capture.test.mjs test/capture-route.test.mjs test/update-resolver.test.mjs test/agent-identity-safe.test.mjs test/re-pair-stale-token.test.mjs test/gpw-folder-policy.test.mjs test/gpw-activation.test.mjs test/gpw-hardening.test.mjs test/gpw-tamper-gate.test.mjs test/gpw-persistence.test.mjs test/gpw-colors.test.mjs test/gpw-recent-fold.test.mjs test/gpw-auto-approve.test.mjs test/data-plane.test.mjs test/sealed-object.test.mjs test/joint-dev-dispatch.test.mjs test/joint-dev-loop.test.mjs test/joint-dev-audit.test.mjs test/joint-dev-redline.test.mjs test/relay-redline-guard.test.mjs test/relay-p0-projection.test.mjs test/guarded-dispatch.test.mjs test/p0-single-matcher.test.mjs test/path-policy.test.mjs test/path-policy-fs.test.mjs test/gpw-inheritance-table.test.mjs test/gpw-policy-adapter.test.mjs test/target-policy.test.mjs test/relay-redline-audit.test.mjs test/relay-repair-ledger.test.mjs test/relay-purge-blobs.test.mjs test/relay-scrub.test.mjs test/joint-dev-plan-store.test.mjs test/relay-basepath-blobs.test.mjs test/operator-token.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 test/exec-capability.test.mjs test/secondary-link.test.mjs test/bootstrap-cli.test.mjs test/bootstrap.test.mjs test/windows-hidden-task.test.mjs test/relay-failover.test.mjs test/npm-update.test.mjs test/agent-start-nonblocking.test.mjs test/fleet-chat-capture.test.mjs test/capture-route.test.mjs test/update-resolver.test.mjs test/agent-identity-safe.test.mjs test/re-pair-stale-token.test.mjs test/gpw-folder-policy.test.mjs test/gpw-activation.test.mjs test/gpw-hardening.test.mjs test/gpw-tamper-gate.test.mjs test/gpw-persistence.test.mjs test/gpw-colors.test.mjs test/gpw-recent-fold.test.mjs test/gpw-auto-approve.test.mjs test/data-plane.test.mjs test/sealed-object.test.mjs test/joint-dev-dispatch.test.mjs test/joint-dev-loop.test.mjs test/joint-dev-audit.test.mjs test/joint-dev-redline.test.mjs test/relay-redline-guard.test.mjs test/relay-p0-projection.test.mjs test/guarded-dispatch.test.mjs test/p0-single-matcher.test.mjs test/path-policy.test.mjs test/path-policy-fs.test.mjs test/gpw-inheritance-table.test.mjs test/gpw-policy-adapter.test.mjs test/target-policy.test.mjs test/relay-redline-audit.test.mjs test/relay-repair-ledger.test.mjs test/relay-purge-blobs.test.mjs test/relay-scrub.test.mjs test/joint-dev-plan-store.test.mjs test/relay-basepath-blobs.test.mjs test/operator-token.test.mjs test/relay-health-capabilities.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",
|
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.9.
|
|
4
|
+
"version": "1.9.16",
|
|
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": [
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relay capability negotiation (v2 G0-2).
|
|
3
|
+
*
|
|
4
|
+
* A joint-dev dispatch names machines on the wire and expects the relay to honour
|
|
5
|
+
* `target_machines`/`executor_machine_id`. An older relay silently ignores those fields, which
|
|
6
|
+
* widens an intended single-machine dispatch to the whole online fleet. The dispatcher therefore
|
|
7
|
+
* reads the relay's advertised features first and refuses when a required feature is missing.
|
|
8
|
+
*
|
|
9
|
+
* A relay with no `features` field at all is older than this protocol: callers decide policy
|
|
10
|
+
* (joint-dev refuses; ordinary dispatch keeps its post-dispatch lease check).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Features a joint-dev dispatch requires from the relay. */
|
|
14
|
+
export const JOINT_REQUIRED_FEATURES = Object.freeze(['target-machines', 'result-projection']);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Read `/healthz` and normalise the capability fields.
|
|
18
|
+
*
|
|
19
|
+
* @param {{rabbitUrl: string, fetchImpl?: Function, timeoutMs?: number}} input
|
|
20
|
+
* @returns {Promise<{ok: boolean, relay_version?: string|null, features?: string[]|null, protocol_version?: number|null, error?: string}>}
|
|
21
|
+
*/
|
|
22
|
+
export async function readRelayCapabilities(input) {
|
|
23
|
+
const base = typeof input?.rabbitUrl === 'string' ? input.rabbitUrl.replace(/\/$/, '') : '';
|
|
24
|
+
if (base === '') return { ok: false, error: 'rabbitUrl is required' };
|
|
25
|
+
const fetchImpl = input?.fetchImpl ?? globalThis.fetch;
|
|
26
|
+
const timeoutMs = Number.isInteger(input?.timeoutMs) ? input.timeoutMs : 15_000;
|
|
27
|
+
try {
|
|
28
|
+
const response = await fetchImpl(`${base}/healthz`, {
|
|
29
|
+
headers: { accept: 'application/json' },
|
|
30
|
+
signal: typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function' ? AbortSignal.timeout(timeoutMs) : undefined,
|
|
31
|
+
});
|
|
32
|
+
if (response?.ok !== true) return { ok: false, error: `healthz answered HTTP ${response?.status ?? '?'}` };
|
|
33
|
+
const doc = await response.json();
|
|
34
|
+
const features = Array.isArray(doc?.features) ? doc.features.filter((entry) => typeof entry === 'string') : null;
|
|
35
|
+
return {
|
|
36
|
+
ok: true,
|
|
37
|
+
relay_version: typeof doc?.relay_version === 'string' ? doc.relay_version : (typeof doc?.version === 'string' ? doc.version : null),
|
|
38
|
+
features,
|
|
39
|
+
protocol_version: Number.isInteger(doc?.protocol_version) ? doc.protocol_version : null,
|
|
40
|
+
operator_token_required: doc?.operator_token_required === true,
|
|
41
|
+
};
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Decide whether the advertised capabilities satisfy a required set.
|
|
49
|
+
*
|
|
50
|
+
* @param {{features?: string[]|null}|null} capabilities
|
|
51
|
+
* @param {string[]} [required]
|
|
52
|
+
* @returns {{ok: boolean, code?: string, missing?: string[], unknown?: boolean}}
|
|
53
|
+
*/
|
|
54
|
+
export function negotiateFeatures(capabilities, required = JOINT_REQUIRED_FEATURES) {
|
|
55
|
+
if (capabilities === null || capabilities?.ok !== true) {
|
|
56
|
+
return { ok: false, code: 'RELAY_CAPABILITIES_UNAVAILABLE', unknown: true };
|
|
57
|
+
}
|
|
58
|
+
const features = capabilities.features;
|
|
59
|
+
if (!Array.isArray(features)) {
|
|
60
|
+
// No advertisement at all: an older relay. Callers that need the fields must not guess.
|
|
61
|
+
return { ok: false, code: 'RELAY_CAPABILITIES_MISSING', missing: [...required], unknown: true };
|
|
62
|
+
}
|
|
63
|
+
const missing = required.filter((feature) => !features.includes(feature));
|
|
64
|
+
if (missing.length > 0) return { ok: false, code: 'RELAY_CAPABILITIES_MISSING', missing, unknown: false };
|
|
65
|
+
return { ok: true };
|
|
66
|
+
}
|
package/src/plugin/tools.mjs
CHANGED
|
@@ -284,7 +284,7 @@ const MAX_STAGES = 16;
|
|
|
284
284
|
* `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
|
|
285
285
|
* overrides it at runtime for tests and packagers.
|
|
286
286
|
*/
|
|
287
|
-
const PLUGIN_VERSION = '1.9.
|
|
287
|
+
const PLUGIN_VERSION = '1.9.16';
|
|
288
288
|
|
|
289
289
|
/** How long any single HTTP call may take before it is aborted, in milliseconds. */
|
|
290
290
|
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|
|
@@ -4895,7 +4895,9 @@ const result = await request({
|
|
|
4895
4895
|
http_status: probe.status,
|
|
4896
4896
|
protocol_version: health.protocol_version ?? null,
|
|
4897
4897
|
error: probe.ok ? null : (probe.error?.code ?? `HTTP ${probe.status}`),
|
|
4898
|
-
relay_id: health.relay_id ?? null,
|
|
4898
|
+
relay_id: health.relay_id ?? null,
|
|
4899
|
+
relay_version: health.relay_version ?? health.version ?? null,
|
|
4900
|
+
features: Array.isArray(health.features) ? health.features : null,
|
|
4899
4901
|
uptime_ms: typeof health.uptime_ms === 'number' ? health.uptime_ms : null,
|
|
4900
4902
|
started_at: health.started_at ?? null,
|
|
4901
4903
|
base_path: health.base_path ?? null,
|
|
@@ -4906,7 +4908,7 @@ const result = await request({
|
|
|
4906
4908
|
persistence: health.persistence ?? null,
|
|
4907
4909
|
// Which of the §4 fields this relay actually sent. A v1 relay sends none of them, and a
|
|
4908
4910
|
// reader should be able to tell "field absent" from "field false".
|
|
4909
|
-
diagnostics_present: ['relay_id', 'uptime_ms', 'base_path', 'effective_scheme', 'operator_token_required']
|
|
4911
|
+
diagnostics_present: ['relay_id', 'uptime_ms', 'base_path', 'effective_scheme', 'operator_token_required', 'relay_version', 'features']
|
|
4910
4912
|
.filter((key) => health[key] !== undefined && health[key] !== null),
|
|
4911
4913
|
};
|
|
4912
4914
|
} catch (error) {
|
package/src/relay/server.mjs
CHANGED
|
@@ -22,6 +22,17 @@ function readRelayVersion() {
|
|
|
22
22
|
return process.env.W2M_RELAY_VERSION ?? 'unknown';
|
|
23
23
|
}
|
|
24
24
|
export const RELAY_VERSION = readRelayVersion();
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Capabilities a dispatcher may rely on. A relay that does not advertise a feature is an older
|
|
28
|
+
* relay: joint-dev must refuse rather than assume the field will be honoured.
|
|
29
|
+
*/
|
|
30
|
+
export const RELAY_FEATURES = Object.freeze([
|
|
31
|
+
'target-machines',
|
|
32
|
+
'result-projection',
|
|
33
|
+
'guard-v1',
|
|
34
|
+
'relay-version-v1',
|
|
35
|
+
]);
|
|
25
36
|
import fs from 'node:fs';
|
|
26
37
|
import os from 'node:os';
|
|
27
38
|
import path from 'node:path';
|
|
@@ -1591,7 +1602,9 @@ export class RelayServer {
|
|
|
1591
1602
|
rabbit_time: this.state.nowIso(),
|
|
1592
1603
|
uptime_ms: this.state.nowMs() - this._startedAtMs,
|
|
1593
1604
|
relay_id: this.relayId,
|
|
1594
|
-
|
|
1605
|
+
version: RELAY_VERSION,
|
|
1606
|
+
relay_version: RELAY_VERSION,
|
|
1607
|
+
features: RELAY_FEATURES,
|
|
1595
1608
|
started_at: rfc3339(this._startedAtMs),
|
|
1596
1609
|
effective_scheme: this.effectiveScheme(req),
|
|
1597
1610
|
base_path: this.basePath,
|