@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.
@@ -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.15"
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",
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.15"
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.15",
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,
@@ -12,6 +12,6 @@
12
12
 
13
13
  [mcp_servers.w2m-fleet]
14
14
  command = "npx"
15
- args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@1.9.15", "w2m-mcp"]
15
+ args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@1.9.16", "w2m-mcp"]
16
16
  startup_timeout_sec = 20
17
17
  tool_timeout_sec = 60
@@ -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.15",
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.15",
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
+ }
@@ -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.15';
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) {
@@ -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
- version: RELAY_VERSION,
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,