@twinsearth/w2m-dsh-plugin 1.2.3 → 1.2.6
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 +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +184 -0
- package/README.md +462 -462
- package/codex/mcp-servers.toml +1 -1
- package/package.json +14 -4
- package/plugin.json +1 -1
- package/src/agent/agent.mjs +162 -4
- package/src/agent/crypto.mjs +31 -0
- package/src/plugin/ask-store.mjs +167 -0
- package/src/plugin/schema-spec.mjs +33 -0
- package/src/plugin/task-graph.mjs +147 -0
- package/src/plugin/tools.mjs +7748 -7385
- package/src/plugin/turn.mjs +41 -0
- package/src/relay/fleet-members.mjs +78 -0
- package/src/relay/fleet-messages.mjs +241 -173
- package/src/relay/report.mjs +5 -0
- package/src/relay/server.mjs +1596 -1498
- package/src/relay/state.mjs +175 -4
- package/src/relay/task-deps.mjs +92 -0
- package/src/util/chat-crypto.mjs +98 -0
- package/src/util/dataflow.mjs +72 -0
- package/src/util/envelope-signature.mjs +69 -0
- package/src/util/policy-matrix.mjs +65 -0
- package/src/util/redact.mjs +70 -0
|
@@ -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.2.
|
|
8
|
+
"version": "1.2.6"
|
|
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.2.
|
|
15
|
+
"version": "1.2.6",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "TwinsEarth"
|
|
18
18
|
},
|
|
@@ -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.2.
|
|
4
|
+
"version": "1.2.6",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TwinsEarth"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,190 @@
|
|
|
7
7
|
|
|
8
8
|
All notable changes to this project are documented here.
|
|
9
9
|
|
|
10
|
+
## [1.2.6] — 2026-10-20
|
|
11
|
+
|
|
12
|
+
**Security surface: signatures, a permission matrix, and output that no longer carries secrets — plus the reliability the chat
|
|
13
|
+
needed before it could be trusted with real messages.**
|
|
14
|
+
|
|
15
|
+
### Command signatures
|
|
16
|
+
|
|
17
|
+
The agent signs `{task_id, argv_hash, machine_id, ts}` with an Ed25519 key derived from its device key (`DeviceKey` is X25519 and
|
|
18
|
+
cannot sign; deriving keeps one secret per machine). The envelope carries `signature {alg, value, public_key, ts}`, and
|
|
19
|
+
`w2m_wait` reports every machine as `signed`, `unsigned` or `bad_signature`, with a task-level `signature_warning`. A tampered
|
|
20
|
+
`argv_hash` verifies as `bad_signature` -- tested at the unit level and end to end through a real agent run.
|
|
21
|
+
|
|
22
|
+
Honest limit: the public half travels inside the signature. That proves the result matches the key it carries, not that the key
|
|
23
|
+
belongs to the machine. Binding it to the relay's device record is the remaining step for non-repudiation across machines, and it
|
|
24
|
+
is named here rather than implied.
|
|
25
|
+
|
|
26
|
+
### Permission matrix
|
|
27
|
+
|
|
28
|
+
`src/util/policy-matrix.mjs` is the single table: `observer < worker < owner` against `read_private | vote | join | dispatch |
|
|
29
|
+
approve | manage_members`. A refusal is `FORBIDDEN_ACTION` naming the action, the level required and the level held. The relay
|
|
30
|
+
enforces it where a role actually acts: an observer may read the fleet flow and may not post into it. Unknown means worker, never
|
|
31
|
+
owner.
|
|
32
|
+
|
|
33
|
+
### Output redaction
|
|
34
|
+
|
|
35
|
+
`stdout_head`/`tail` and `stderr_head`/`tail` are filtered for API keys, `npm_`/`github_pat_` tokens and private-key blocks,
|
|
36
|
+
replaced in place with `[redacted:class]` so line numbers survive. Hashes and blobs are never touched: they are evidence of the raw
|
|
37
|
+
bytes. `redactOutput: false` is allowed and marks the envelope `unredacted: true`.
|
|
38
|
+
|
|
39
|
+
### End-to-end encrypted chat
|
|
40
|
+
|
|
41
|
+
Sending encrypts with AES-256-GCM under a group key derived as `HKDF(fleetKey, 'fleet-chat-v1')`; the relay stores ciphertext and
|
|
42
|
+
cannot open it. Reading decrypts what the key covers and marks the rest `encrypted: true` with a `decrypt_error`. Plaintext
|
|
43
|
+
messages keep working, so older clients neither break nor are lied to. The relay validates the encryption envelope's shape and
|
|
44
|
+
refuses a malformed one (`BAD_ENCRYPTION`), and cannot search content -- which is the point, and is written down as the cost.
|
|
45
|
+
|
|
46
|
+
### Reliability
|
|
47
|
+
|
|
48
|
+
- **Role persistence**: `member.role` ledger entries replay on restart, so a relay restart no longer silently demotes every owner.
|
|
49
|
+
Verified live: the role that was lost before the deploy came back after it.
|
|
50
|
+
- **Chat cursor**: `list` continues where the last page ended (`since_ms` from a persisted per-session cursor) and advances only
|
|
51
|
+
over what was actually read.
|
|
52
|
+
- **Offline spool**: a send that cannot reach the relay is written to disk with an idempotency key and replayed on the next call;
|
|
53
|
+
a replay that the relay already received returns the first message instead of posting twice.
|
|
54
|
+
|
|
55
|
+
### Verified
|
|
56
|
+
|
|
57
|
+
Gate GREEN twice, 15 of 15: 2666 tests / 2652 pass / 0 fail / 14 skip in 130 files. New suites: chat-crypto (8),
|
|
58
|
+
envelope-signature (6), policy-matrix (9); agent 152, relay 173, tools 132, fleet-messages 14.
|
|
59
|
+
|
|
60
|
+
Relay redeployed twice from hash-verified tarballs, with state backups, and the new surfaces exercised against the live relay:
|
|
61
|
+
encrypted message stored as ciphertext and read back, malformed encryption refused with 400, roles restored after restart.
|
|
62
|
+
|
|
63
|
+
### Known gaps
|
|
64
|
+
|
|
65
|
+
- The GitHub Packages mirror still holds an orphan `1.2.4` published by a diagnostic dispatch; removal needs a token with package
|
|
66
|
+
administration (this one sees 404 on the package API).
|
|
67
|
+
- The signing public key is not yet bound to the relay's device record (see above).
|
|
68
|
+
- Two-machine end-to-end runs of the new chat surface are still not done; the HTTP and tool surfaces are tested against fakes and
|
|
69
|
+
exercised once against jp-shared.
|
|
70
|
+
## [1.2.5] — 2026-10-20
|
|
71
|
+
|
|
72
|
+
**Who is in the fleet, what they have read, who was named, and a retry that cannot double-post.**
|
|
73
|
+
|
|
74
|
+
### Members and roles
|
|
75
|
+
|
|
76
|
+
`GET /v1/fleet/members` lists every machine with its name, role, join time, presence and last-seen time. `PUT
|
|
77
|
+
/v1/fleet/members/{id}` changes a role and requires the **operator** token, so the route sits with dispatch rather than behind the
|
|
78
|
+
device gate. Roles are `owner | worker | observer` and live in their own table: a device record is written by pairing and by
|
|
79
|
+
reconnect, and a role is an operator decision that must survive both. An unknown machine is a `worker`, never an `owner` -- a
|
|
80
|
+
re-pair cannot gain authority. The last owner cannot be demoted, because then nobody could readmit a machine.
|
|
81
|
+
|
|
82
|
+
### Read watermarks
|
|
83
|
+
|
|
84
|
+
`PUT /v1/fleet/read/{session}` records how far this machine has read a conversation (its own token decides which machine);
|
|
85
|
+
`GET /v1/fleet/read/{session}` answers the watermark and an unread count of messages from others newer than it. Only forward: a
|
|
86
|
+
stale marker arriving late must not move the watermark backwards, or the count would grow for messages already seen.
|
|
87
|
+
|
|
88
|
+
### Mentions
|
|
89
|
+
|
|
90
|
+
A message may carry `mentions: [machine_id]` (at most 10, validated). The row is still **one** row -- a mention is a
|
|
91
|
+
notification hint, not a delivery mechanism -- and the SSE notification says only whether **this** listener was named. It never
|
|
92
|
+
carries the text, and it does not carry the mention list.
|
|
93
|
+
|
|
94
|
+
### Idempotent posts
|
|
95
|
+
|
|
96
|
+
`POST /v1/fleet/messages` accepts `idempotency_key` (per session). A retry after a lost response returns the **first** message
|
|
97
|
+
with `deduped: true` instead of storing the sentence twice. The key is bounded, like the message log.
|
|
98
|
+
|
|
99
|
+
### Two defects the 1.2.4 tag carried, fixed here
|
|
100
|
+
|
|
101
|
+
The 1.2.4 commit (tag `v1.2.4`) had a red CI and a red Compat, both found before approval:
|
|
102
|
+
|
|
103
|
+
- **Compat**: `w2m_wait`'s `reducer_machine_id` declaration had been pasted **inside** `poll_ms`. Every local check was green;
|
|
104
|
+
the real host refused the whole entry ("parameters.poll_ms.reducer_machine_id is not supported by the value schema DSL"). The
|
|
105
|
+
parameter now sits where it belongs, and `schema-spec.mjs` gained `strayParameterProblems()` — a parameter declaration
|
|
106
|
+
nested inside another parameter is now refused where the tool is written, with two tests.
|
|
107
|
+
- **CI**: the cancellation HTTP test had been inserted inside an enclosing `it`. Node 24 ran it; Node 20/22 reported
|
|
108
|
+
`cancelledByParent`. The test now lives in its own top-level `describe`.
|
|
109
|
+
|
|
110
|
+
### Verified
|
|
111
|
+
|
|
112
|
+
Gate GREEN twice, 15 of 15: 2628 tests / 2614 pass / 0 fail / 14 skip. New suites: fleet-members (6); fleet-messages grew to 11;
|
|
113
|
+
relay grew to 171 including two HTTP cases for members and watermarks.
|
|
114
|
+
|
|
115
|
+
The relay was deployed a fourth time from a hash-verified tarball with a state backup, and the new routes were exercised against
|
|
116
|
+
the live relay (members returned 8 machines; a role change returned 200; a watermark round-tripped; unread answered 0).
|
|
117
|
+
|
|
118
|
+
### Not in this release
|
|
119
|
+
|
|
120
|
+
- **Offline replay of outbound chat** is not implemented. The agent's spool already holds undeliverable *results*; chat text is
|
|
121
|
+
not yet spooled and replayed.
|
|
122
|
+
- A two-machine end-to-end run of mentions/watermarks. The HTTP surface is tested against the fake relay and exercised once
|
|
123
|
+
against jp-shared, but not by two physical machines.
|
|
124
|
+
## [1.2.4] — 2026-10-20
|
|
125
|
+
|
|
126
|
+
**Fleet collaboration, part two: cancellation, dependencies, a task board, questions on disk, and output that travels.**
|
|
127
|
+
|
|
128
|
+
### Cancelling a task over HTTP
|
|
129
|
+
|
|
130
|
+
`state.cancelTask` has existed since the task ledger gained a cancel flag, but **no route reached it**. `POST /v1/tasks/{id}/cancel`
|
|
131
|
+
now does, with the same operator authorization dispatch requires, and `w2m_cancel` is the tool over it. Cancelling twice is not
|
|
132
|
+
an error: the ledger is the answer.
|
|
133
|
+
|
|
134
|
+
### Prerequisites and a task board
|
|
135
|
+
|
|
136
|
+
- `POST /v1/task` accepts `depends_on[]`. Validated at **enqueue** time: unknown prerequisite, self-edge, duplicate edge and
|
|
137
|
+
cycles are refused by name (`src/relay/task-deps.mjs`), because a graph checked only when a worker picks the task up puts the
|
|
138
|
+
refusal on a machine that did not create the problem.
|
|
139
|
+
- `depends_on` / `assignee` travel on the task record.
|
|
140
|
+
- `w2m_tasks` (the fifteenth tool) is the board: it classifies every visible task as ready / active / waiting / blocked / done
|
|
141
|
+
(`src/plugin/task-graph.mjs`). A prerequisite outside the page is treated as **waiting**, never as absent.
|
|
142
|
+
|
|
143
|
+
### Questions that survive a restart
|
|
144
|
+
|
|
145
|
+
`src/plugin/ask-store.mjs` keeps open questions in a JSONL file next to the profile (`ask-store.jsonl`, atomic rewrite, cap on
|
|
146
|
+
records). A reply that arrives after the deadline is recorded as `answered_after_timeout` rather than overwriting the timeout or
|
|
147
|
+
being dropped, and a settled question is never reopened. `w2m_chat` gained `action: "asks"`, and `await` settles the stored ask.
|
|
148
|
+
|
|
149
|
+
### Output that travels
|
|
150
|
+
|
|
151
|
+
`src/util/dataflow.mjs` (moved from `src/plugin/` — the layer check refused an `agent -> plugin` edge) is now wired:
|
|
152
|
+
|
|
153
|
+
- a pipeline stage whose stdout exceeds 64 KiB **uploads it as a blob** (`PUT /v1/fleet/blobs/{sha256}`) and the envelope names
|
|
154
|
+
it (`blob_sha256`, `output_mode: "blob"`) instead of inlining head/tail;
|
|
155
|
+
- a failed upload **fails the stage** by name (`BLOB_UPLOAD_FAILED`) rather than leaving a reference to nothing;
|
|
156
|
+
- `w2m_wait` builds the reduce plan with `buildReducePlan` from the **raw** envelopes and marks the verdict `degraded` when a
|
|
157
|
+
shard is missing — but only when the caller declared `reducer_machine_id`, so a large pipeline result is not mistaken for a
|
|
158
|
+
partial reduce.
|
|
159
|
+
|
|
160
|
+
The end-to-end fixture this release adds (`test/agent.test.mjs`) runs a real agent against a fake relay: it asserts the blob name
|
|
161
|
+
is the hash of the bytes sent and that a refused upload fails the stage. Two real defects surfaced while writing it and are fixed
|
|
162
|
+
here: the stage stdout arrives as a `Buffer` (it was handed to the signature helper as a string), and a 70 KiB test argv exceeds
|
|
163
|
+
Windows' `CreateProcess` limit.
|
|
164
|
+
|
|
165
|
+
### Retry and hand-off
|
|
166
|
+
|
|
167
|
+
- `POST /v1/task` accepts `max_attempts` (1..5, default 1) and `retry_on` (`expired|failed|partial|timeout`). Defaults keep
|
|
168
|
+
at-most-once for every existing caller; a malformed policy is `BAD_REQUEST`, not silently ignored.
|
|
169
|
+
- On lease expiry, `retry_on` containing `expired` performs the hand-off `halt=now` always had: attempt+1 and a re-offer to idle
|
|
170
|
+
machines (`_takeover`, which already refuses writing tasks to avoid double application).
|
|
171
|
+
|
|
172
|
+
### mapreduce
|
|
173
|
+
|
|
174
|
+
`mode: "mapreduce"` is accepted with `reduce_argv` and an optional `reducer_machine_id`. The map half is ordinary dispatch; the
|
|
175
|
+
reduce half is **described, never launched** — the caller runs it as an explicit second task after `w2m_wait` says every shard is
|
|
176
|
+
present, so one task id never describes two executions.
|
|
177
|
+
|
|
178
|
+
### Relay
|
|
179
|
+
|
|
180
|
+
The relay was deployed three times during this release (cancel route, retry policy, mapreduce) from hash-verified tarballs, each
|
|
181
|
+
with a state backup, a health check, and a 401 check on the pre-existing routes. **The relay name is separate from the package
|
|
182
|
+
version**: the tarball deployed was the 1.2.3-labelled build carrying this code, and is re-deployed from the released 1.2.4 tarball
|
|
183
|
+
below.
|
|
184
|
+
|
|
185
|
+
### Verified
|
|
186
|
+
|
|
187
|
+
Gate GREEN twice, 15 of 15: 2610 tests / 2596 pass / 0 fail / 14 skip. Notable suites: agent 149 (147 pass, 2 skipped),
|
|
188
|
+
relay 169, tools 127, task-deps 13, task-graph 11, ask-store 8, turn 5, dataflow 7, dataflow-wiring 7.
|
|
189
|
+
|
|
190
|
+
### Not verified
|
|
191
|
+
|
|
192
|
+
A two-machine run of cancel / retry / mapreduce end to end. The code paths are tested against a fake relay and against the real
|
|
193
|
+
relay's HTTP surface, but no test here runs two physical machines exchanging an expired lease.
|
|
10
194
|
## [1.2.3] — 2026-10-20
|
|
11
195
|
|
|
12
196
|
**The packed artifact did not mount. This fixes that, and the reason it was not caught here.**
|