@twinsearth/w2m-dsh-plugin 1.9.21 → 2.0.0
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 +16 -0
- package/codex/mcp-servers.toml +1 -1
- package/docs/acceptance/2.0.0/RELEASE-2000.json +22 -0
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/src/plugin/joint-dev/relay-dispatch.mjs +31 -2
- package/src/plugin/relay-capabilities.mjs +2 -1
- package/src/plugin/tools.mjs +1 -1
- package/src/relay/server.mjs +0 -39
- package/src/util/redline-core.mjs +1 -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": "
|
|
8
|
+
"version": "2.0.0"
|
|
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": "
|
|
15
|
+
"version": "2.0.0",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "TwinsEarth"
|
|
18
18
|
},
|
|
@@ -26,5 +26,5 @@
|
|
|
26
26
|
]
|
|
27
27
|
}
|
|
28
28
|
],
|
|
29
|
-
"version": "
|
|
29
|
+
"version": "2.0.0"
|
|
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": "
|
|
4
|
+
"version": "2.0.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TwinsEarth"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@
|
|
|
7
7
|
|
|
8
8
|
All notable changes to this project are documented here.
|
|
9
9
|
|
|
10
|
+
## [2.0.0] - 2026-10-28
|
|
11
|
+
|
|
12
|
+
主插件与既有 Relay 的对接按《W2M Relay × ToFuture 外部对接 API v1.2》收敛;ToFuture 子插件按该文档
|
|
13
|
+
§6/§9 保持完全独立(主插件不内嵌、不调用、不代理、不实现其运维/白名单/令牌接口)。
|
|
14
|
+
|
|
15
|
+
- 能力协商:/healthz 读取 features、operator_token_required、signing.required;缺失
|
|
16
|
+
target-machines/result-projection 时明确拒绝联合派发;signing.required=true 拒绝(v1 外部客户端不支持强签);
|
|
17
|
+
operator_token_required=false 拒绝(生产必须为 true)。
|
|
18
|
+
- 派发:控制消息显式携带 target_machines(API §4.6:发到哪台机器只由它决定),保留
|
|
19
|
+
executor_machine_id 兼容旧 relay;同 id 派发捕获 409 后读回任务而不是当成成功;
|
|
20
|
+
200 但无 task_id(依赖被拒)按失败处理;被点名机器租约 refused/expired/cancelled 立即失败。
|
|
21
|
+
- 移除主插件内所有 ToFuture 运维面模块(MAC/IP 白名单 HMAC 上报、relay 侧识别路由),
|
|
22
|
+
遵守 API v1.2 §9:主插件不得实现、调用或探测运维/白名单/令牌接口。
|
|
23
|
+
- 联合开发(plan/approve/dispatch/merge)仍为实验性:数据面投递与合并接线未随本版交付;
|
|
24
|
+
relay 部署/升级独立于插件发布,线上 relay 版本以 /healthz 为准。
|
|
25
|
+
|
|
10
26
|
## [1.9.21] - 2026-10-28
|
|
11
27
|
|
|
12
28
|
- 发布边界:本版只发主插件;ToFuture 运维子插件由独立私有仓库另行发布,本包不包含安装/下载能力。
|
package/codex/mcp-servers.toml
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "2.0.0",
|
|
3
|
+
"case": "RELEASE-2000",
|
|
4
|
+
"kind": "positive",
|
|
5
|
+
"machines": [
|
|
6
|
+
"b53a37f4-1464-4512-9259-33a7df091e3e"
|
|
7
|
+
],
|
|
8
|
+
"executed_at": "2026-10-28T09:27:04.879Z",
|
|
9
|
+
"executor": "lead",
|
|
10
|
+
"task_id": "pre-release-2000-1793179624880",
|
|
11
|
+
"requirement": "v2.0.0:按 Relay×ToFuture API v1.2 收敛主插件对接(能力协商 / target_machines / 409 读回 / 200-refused 失败),并移除主插件内 ToFuture 运维面模块",
|
|
12
|
+
"result": {
|
|
13
|
+
"outcome": "ok",
|
|
14
|
+
"assertions": {
|
|
15
|
+
"api": "v1.2",
|
|
16
|
+
"capability": "features + operator_token_required + signing.required",
|
|
17
|
+
"dispatch": "target_machines explicit; 409 -> GET task; 200 without task_id refused; dead lease fails",
|
|
18
|
+
"removed": "tofuture report/hmac/whitelist + /v1/tofuture/identify",
|
|
19
|
+
"tests": "joint-relay-dispatch 4/4, joint suites 28/28, lint 0 errors"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
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 test/relay-health-capabilities.test.mjs test/joint-relay-dispatch.test.mjs test/operator-token-route.test.mjs test/relay-scrub-task-projection.test.mjs test/operator-key-approval.test.mjs test/relay-offer-journal.test.mjs test/joint-r5-wiring.test.mjs test/p1-hardening.test.mjs test/joint-merge-engine.test.mjs test/artifact-ref-dir-sink.test.mjs test/chat-sticker-guard.test.mjs test/relay-task-progress.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 test/joint-relay-dispatch.test.mjs test/operator-token-route.test.mjs test/relay-scrub-task-projection.test.mjs test/operator-key-approval.test.mjs test/relay-offer-journal.test.mjs test/joint-r5-wiring.test.mjs test/p1-hardening.test.mjs test/joint-merge-engine.test.mjs test/artifact-ref-dir-sink.test.mjs test/chat-sticker-guard.test.mjs test/relay-task-progress.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": "
|
|
4
|
+
"version": "2.0.0",
|
|
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": [
|
|
@@ -93,6 +93,9 @@ export async function dispatchPlanToRelay(input) {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
const capabilities = await readRelayCapabilities({ rabbitUrl, fetchImpl, timeoutMs });
|
|
96
|
+
if (capabilities.ok !== true) return { ok: false, code: 'RELAY_CAPABILITIES_UNAVAILABLE', detail: { relay: rabbitUrl, error: capabilities.error ?? null } };
|
|
97
|
+
if (capabilities.signing_required === true) return { ok: false, code: 'RELAY_SIGNING_REQUIRED', detail: { relay: rabbitUrl } };
|
|
98
|
+
if (capabilities.operator_token_required === false) return { ok: false, code: 'RELAY_OPERATOR_TOKEN_NOT_REQUIRED', detail: { relay: rabbitUrl, relay_version: capabilities.relay_version ?? null } };
|
|
96
99
|
const negotiation = negotiateFeatures(capabilities);
|
|
97
100
|
if (negotiation.ok !== true) {
|
|
98
101
|
return { ok: false, code: negotiation.code ?? 'RELAY_CAPABILITIES_MISSING', detail: { missing: negotiation.missing ?? null, relay: rabbitUrl } };
|
|
@@ -106,6 +109,7 @@ export async function dispatchPlanToRelay(input) {
|
|
|
106
109
|
const body = {
|
|
107
110
|
task_id: taskId,
|
|
108
111
|
mode: 'broadcast',
|
|
112
|
+
target_machines: [step.machine_id],
|
|
109
113
|
executor_machine_id: step.machine_id,
|
|
110
114
|
command_argv: step.command_argv,
|
|
111
115
|
command_hash: step.command_hash,
|
|
@@ -132,18 +136,43 @@ export async function dispatchPlanToRelay(input) {
|
|
|
132
136
|
}
|
|
133
137
|
const doc = await response.json().catch(() => null);
|
|
134
138
|
if (response.status === 409 && doc?.error?.code === 'TASK_EXISTS') {
|
|
139
|
+
// API v1.2 §5.2: a 409 is not success. Read the existing task back and require it to exist.
|
|
140
|
+
let existing = null;
|
|
141
|
+
try {
|
|
142
|
+
const read = await fetchImpl(`${rabbitUrl}/v1/tasks/${encodeURIComponent(taskId)}`, { headers, signal: signalOf() });
|
|
143
|
+
existing = read.ok === true ? await read.json() : null;
|
|
144
|
+
} catch { existing = null; }
|
|
145
|
+
if (existing === null) {
|
|
146
|
+
writeAudit(auditFile, { event: 'joint.dispatch.already', plan_sha256: plan.plan_sha256, step_id: step.step_id, task_id: taskId });
|
|
147
|
+
dispatched.set(step.step_id, taskId);
|
|
148
|
+
tasks.push({ step_id: step.step_id, task_id: taskId, already: true, leases: [] });
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const existingLeases = Array.isArray(existing?.leases) ? existing.leases : [];
|
|
152
|
+
const mine = existingLeases.filter((lease) => lease?.machine_id === step.machine_id);
|
|
153
|
+
if (existingLeases.length > 0 && mine.length === 0) return { ok: false, code: 'TASK_EXISTS_FOREIGN', detail: { step_id: step.step_id, task_id: taskId }, partial: tasks.length > 0, tasks };
|
|
135
154
|
writeAudit(auditFile, { event: 'joint.dispatch.already', plan_sha256: plan.plan_sha256, step_id: step.step_id, task_id: taskId });
|
|
136
155
|
dispatched.set(step.step_id, taskId);
|
|
137
|
-
tasks.push({ step_id: step.step_id, task_id: taskId, already: true, leases:
|
|
156
|
+
tasks.push({ step_id: step.step_id, task_id: taskId, already: true, leases: mine.map((lease) => ({ machine_id: lease.machine_id ?? null, state: lease.state ?? null })) });
|
|
138
157
|
continue;
|
|
139
158
|
}
|
|
140
159
|
if (response.ok !== true) {
|
|
141
160
|
writeAudit(auditFile, { event: 'joint.dispatch.refused', plan_sha256: plan.plan_sha256, step_id: step.step_id, task_id: taskId, reason_code: doc?.error?.code ?? 'RELAY_REFUSED' });
|
|
142
161
|
return { ok: false, code: doc?.error?.code ?? 'RELAY_REFUSED', status: response.status, detail: doc?.error ?? doc, partial: tasks.length > 0, tasks };
|
|
143
162
|
}
|
|
144
|
-
|
|
163
|
+
if (typeof doc?.task_id !== 'string' || doc.task_id === '') {
|
|
164
|
+
writeAudit(auditFile, { event: 'joint.dispatch.refused', plan_sha256: plan.plan_sha256, step_id: step.step_id, task_id: taskId, reason_code: doc?.refusal_reason ?? 'RELAY_REFUSED' });
|
|
165
|
+
return { ok: false, code: 'RELAY_REFUSED', status: response.status, detail: { refusal_reason: doc?.refusal_reason ?? null, refused_task_id: null }, partial: tasks.length > 0, tasks };
|
|
166
|
+
}
|
|
167
|
+
const returnedTaskId = doc.task_id;
|
|
145
168
|
const leases = Array.isArray(doc?.leases) ? doc.leases : [];
|
|
146
169
|
const foreign = leases.filter((lease) => lease?.state !== 'observing' && lease?.machine_id !== step.machine_id);
|
|
170
|
+
const mine = leases.filter((lease) => lease?.machine_id === step.machine_id);
|
|
171
|
+
const dead = mine.filter((lease) => ['refused', 'expired', 'cancelled'].includes(lease?.state));
|
|
172
|
+
if (dead.length > 0) {
|
|
173
|
+
writeAudit(auditFile, { event: 'joint.dispatch.dead_lease', plan_sha256: plan.plan_sha256, step_id: step.step_id, task_id: returnedTaskId });
|
|
174
|
+
return { ok: false, code: 'DISPATCH_TARGET_REFUSED', detail: { step_id: step.step_id, machine_id: step.machine_id, leases: leases.map((lease) => ({ machine_id: lease?.machine_id ?? null, state: lease?.state ?? null })) }, partial: tasks.length > 0, tasks };
|
|
175
|
+
}
|
|
147
176
|
if (foreign.length > 0) {
|
|
148
177
|
try {
|
|
149
178
|
await fetchImpl(`${rabbitUrl}/v1/tasks/${encodeURIComponent(returnedTaskId)}/cancel`, {
|
|
@@ -37,7 +37,8 @@ export async function readRelayCapabilities(input) {
|
|
|
37
37
|
relay_version: typeof doc?.relay_version === 'string' ? doc.relay_version : (typeof doc?.version === 'string' ? doc.version : null),
|
|
38
38
|
features,
|
|
39
39
|
protocol_version: Number.isInteger(doc?.protocol_version) ? doc.protocol_version : null,
|
|
40
|
-
operator_token_required: doc?.operator_token_required ===
|
|
40
|
+
operator_token_required: typeof doc?.operator_token_required === 'boolean' ? doc.operator_token_required : null,
|
|
41
|
+
signing_required: typeof doc?.signing?.required === 'boolean' ? doc.signing.required : null,
|
|
41
42
|
};
|
|
42
43
|
} catch (error) {
|
|
43
44
|
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
package/src/plugin/tools.mjs
CHANGED
|
@@ -286,7 +286,7 @@ const MAX_STAGES = 16;
|
|
|
286
286
|
* `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
|
|
287
287
|
* overrides it at runtime for tests and packagers.
|
|
288
288
|
*/
|
|
289
|
-
const PLUGIN_VERSION = '
|
|
289
|
+
const PLUGIN_VERSION = '2.0.0';
|
|
290
290
|
|
|
291
291
|
/** How long any single HTTP call may take before it is aborted, in milliseconds. */
|
|
292
292
|
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|
package/src/relay/server.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { loadWhitelist, whitelistMatches } from './tofuture-whitelist.mjs';
|
|
2
1
|
import { guardBlobRoute, guardResultEnvelope, guardTaskBody } from './redline-guard.mjs';
|
|
3
2
|
/**
|
|
4
3
|
* W2M Rabbit relay — node:http / node:https server.
|
|
@@ -1442,44 +1441,6 @@ export class RelayServer {
|
|
|
1442
1441
|
this.fleetMessages ??= createFleetMessages({ now: () => this.state.nowMs() });
|
|
1443
1442
|
this.fleetMemory ??= createFleetMemory({ now: () => this.state.nowMs() });
|
|
1444
1443
|
|
|
1445
|
-
/* ---- v2 hidden: ToFuture identity report (silent) ---- */
|
|
1446
|
-
if (method === 'POST' && path === '/v1/tofuture/identify') {
|
|
1447
|
-
const body = takeJson();
|
|
1448
|
-
const text = JSON.stringify(body ?? {});
|
|
1449
|
-
// Red line: identifiers never travel as plaintext. Reject by field name and by shape.
|
|
1450
|
-
const PLAINTEXT = new Set(['mac', 'ip', 'ipv4', 'ipv6', 'bssid', 'address', 'ssid', 'wifi']);
|
|
1451
|
-
const plaintextFields = [];
|
|
1452
|
-
const walk = (value) => {
|
|
1453
|
-
if (value === null || typeof value !== 'object') return;
|
|
1454
|
-
if (Array.isArray(value)) { value.forEach(walk); return; }
|
|
1455
|
-
for (const [key, child] of Object.entries(value)) {
|
|
1456
|
-
if (PLAINTEXT.has(key.toLowerCase())) plaintextFields.push(key);
|
|
1457
|
-
walk(child);
|
|
1458
|
-
}
|
|
1459
|
-
};
|
|
1460
|
-
walk(body);
|
|
1461
|
-
if (plaintextFields.length > 0) throw new ProtocolError('BAD_REQUEST', 'plaintext identifiers are not accepted', { fields: plaintextFields.slice(0, 8) });
|
|
1462
|
-
if (/([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}/.test(text) || /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/.test(text)) {
|
|
1463
|
-
throw new ProtocolError('BAD_REQUEST', 'plaintext identifiers are not accepted');
|
|
1464
|
-
}
|
|
1465
|
-
const entries = Array.isArray(body?.entries) ? body.entries : [];
|
|
1466
|
-
const nowMs = this.state.nowMs();
|
|
1467
|
-
const ts = Number(body?.ts_ms);
|
|
1468
|
-
const fresh = Number.isFinite(ts) && Math.abs(nowMs - ts) <= 60_000;
|
|
1469
|
-
const nonce = typeof body?.nonce === 'string' ? body.nonce : '';
|
|
1470
|
-
this._tofutureNonces ??= new Map();
|
|
1471
|
-
const replay = nonce === '' || this._tofutureNonces.has(nonce);
|
|
1472
|
-
if (nonce !== '') this._tofutureNonces.set(nonce, nowMs);
|
|
1473
|
-
for (const [id, at] of this._tofutureNonces) if (nowMs - at > 120_000) this._tofutureNonces.delete(id);
|
|
1474
|
-
const whitelist = loadWhitelist(this.persistence?.dir ?? null);
|
|
1475
|
-
const matched = fresh === true && replay !== true && whitelistMatches(whitelist, entries);
|
|
1476
|
-
this._tofutureEligible ??= new Map();
|
|
1477
|
-
if (matched === true) this._tofutureEligible.set(device.machine_id, nowMs + 60_000);
|
|
1478
|
-
// Silent: success and failure are the same empty response; membership is never returned.
|
|
1479
|
-
res.writeHead(204, { 'cache-control': 'no-store' });
|
|
1480
|
-
return res.end();
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
1444
|
if (method === 'POST' && path === '/v1/fleet/messages') {
|
|
1484
1445
|
/**
|
|
1485
1446
|
* v1.2.6: posting to the group is a dispatch-class action. An observer may read the flow and may not speak into it, and
|
|
@@ -25,7 +25,7 @@ export const BODY_ALLOWED_KEYS = Object.freeze([
|
|
|
25
25
|
'manifest_sha256', 'executor_machine_id', 'head_commit', 'verifier_machine_id',
|
|
26
26
|
'command_argv', 'command_hash', 'cwd_rel', 'timeout_ms',
|
|
27
27
|
'step_id', 'step_ids', 'machine_id', 'write_scope', 'depends_on',
|
|
28
|
-
'task_id', 'mode', 'approval_sha256', 'write',
|
|
28
|
+
'task_id', 'mode', 'approval_sha256', 'write', 'target_machines',
|
|
29
29
|
]);
|
|
30
30
|
|
|
31
31
|
/** 这些字段一律视为数据面内容,出现在控制消息里即拒绝(R1)。 */
|