@twinsearth/w2m-dsh-plugin 1.1.20 → 1.1.22
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 +29 -29
- package/.claude-plugin/plugin.json +29 -29
- package/CHANGELOG.md +66 -2
- package/codex/mcp-servers.toml +1 -1
- package/lib/client.js +28 -2
- package/package.json +218 -214
- package/plugin.json +14 -14
- package/src/plugin/fleet-chat-route.mjs +48 -16
- package/src/plugin/session-watch.mjs +28 -2
- package/src/plugin/tools.mjs +23 -7
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "twinsearth-w2m",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "TwinsEarth"
|
|
5
|
+
},
|
|
6
|
+
"metadata": {
|
|
7
|
+
"description": "The w2m fleet plugins: one account, many machines, any of Windows, macOS or Linux.",
|
|
8
|
+
"version": "1.1.22"
|
|
9
|
+
},
|
|
10
|
+
"plugins": [
|
|
11
|
+
{
|
|
12
|
+
"name": "w2m-fleet",
|
|
13
|
+
"source": "./",
|
|
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.1.22",
|
|
16
|
+
"author": {
|
|
17
|
+
"name": "TwinsEarth"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/TwinsEarth/dsh-windows2macos",
|
|
20
|
+
"repository": "https://github.com/TwinsEarth/dsh-windows2macos",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"keywords": [
|
|
23
|
+
"fleet",
|
|
24
|
+
"remote-execution",
|
|
25
|
+
"mcp"
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "w2m-fleet",
|
|
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.1.22",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "TwinsEarth"
|
|
7
|
+
},
|
|
8
|
+
"homepage": "https://github.com/TwinsEarth/dsh-windows2macos",
|
|
9
|
+
"repository": "https://github.com/TwinsEarth/dsh-windows2macos",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"fleet",
|
|
13
|
+
"remote-execution",
|
|
14
|
+
"windows",
|
|
15
|
+
"macos",
|
|
16
|
+
"linux",
|
|
17
|
+
"mcp",
|
|
18
|
+
"claude-code",
|
|
19
|
+
"codex"
|
|
20
|
+
],
|
|
21
|
+
"mcpServers": {
|
|
22
|
+
"w2m-fleet": {
|
|
23
|
+
"command": "node",
|
|
24
|
+
"args": [
|
|
25
|
+
"${CLAUDE_PLUGIN_ROOT}/bin/w2m-mcp.mjs"
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,64 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented here.
|
|
4
4
|
|
|
5
|
-
## [1.1.
|
|
5
|
+
## [1.1.22] — 2026-10-18
|
|
6
|
+
|
|
7
|
+
**The fleet-chat panel said "this machine has no fleet-chat identity yet" on a machine that was paired and online.**
|
|
8
|
+
|
|
9
|
+
Two structural causes, both introduced by 1.1.20's move to a conversation key:
|
|
10
|
+
|
|
11
|
+
- **identity was read with the wrong reader.** The panel's identity came from `readDevice({ stateDir })`, which returns
|
|
12
|
+
empty the moment `stateDir` is unset -- and unset is the documented default layout, because the agent writes its identity
|
|
13
|
+
to `<DSH_HOME>/xclient/device.json`. The plugin already had `readLocalIdentity(cfg)`, which knows that fallback and
|
|
14
|
+
says so in its own documentation; it was simply not used here. The same bug made `w2m_status` report `machine_id: null`
|
|
15
|
+
next to a working P2P identity.
|
|
16
|
+
- **the account material was read from a scope that cannot see it.** The conversation key is derived from the **fleet
|
|
17
|
+
key**, which `startLocalStack` resolves at stack start. The composer route is registered in `apply()`, one scope above
|
|
18
|
+
it, so all it could read was `cfg.fleetKey` -- null on a machine that joined by account. It now reads the resolved key
|
|
19
|
+
through a holder the stack updates, so the route and the join agree by construction.
|
|
20
|
+
|
|
21
|
+
### The fallback, and why it is written down
|
|
22
|
+
|
|
23
|
+
A missing account key no longer refuses. The conversation is then keyed by the machine id, and the response says
|
|
24
|
+
`conversation_source: "machine"` instead of leaving the caller to guess. A panel that cannot answer at all is worse than
|
|
25
|
+
one that answers for this machine until the account join lands; the moment the fleet key resolves the key changes to the
|
|
26
|
+
account scope, which is what makes two machines in one project one conversation.
|
|
27
|
+
|
|
28
|
+
### Verified
|
|
29
|
+
|
|
30
|
+
The new `conversationBasis` contract: account basis when the key exists, machine basis with an explicit source when it does
|
|
31
|
+
not, and `null` only when there is no machine at all. Tools 122/122, client-bundle 25/25, fleet-chat-identity 4/4.
|
|
32
|
+
|
|
33
|
+
### Not verified
|
|
34
|
+
|
|
35
|
+
The panel against a live host, which is what this beta is for: the machine that reported the error is the machine that will
|
|
36
|
+
report whether it is fixed.
|
|
37
|
+
|
|
38
|
+
## [1.1.21] — 2026-10-18
|
|
39
|
+
|
|
40
|
+
**A fix-forward release: 1.1.20 could not load on Node 20.**
|
|
41
|
+
|
|
42
|
+
1.1.20 shipped with `import { zstdDecompressSync } from 'node:zlib'`. That named export does not exist on Node 20, so the
|
|
43
|
+
module did not merely lose a feature -- the whole entry-point chain failed to import ("lib/tools.js" and "lib/index.js"
|
|
44
|
+
both refused to load). CI caught it on `windows-latest / node 20` after 1.1.20 had already been published, and npm does
|
|
45
|
+
not allow a version to be replaced, so this release fixes forward.
|
|
46
|
+
|
|
47
|
+
- the zstd API is now resolved lazily through `require` behind `zstdAvailable()`; on a runtime without it,
|
|
48
|
+
`decodeTranscript` returns nothing instead of crashing, and the session mirror simply publishes no prompts;
|
|
49
|
+
- the reader tests skip themselves on such a runtime rather than failing to import;
|
|
50
|
+
- the release pipeline was re-examined afterwards: a beta needs no approval, a stable release is approved in the
|
|
51
|
+
`Stable` environment, and this time CI was green **before** the approval was requested, not after.
|
|
52
|
+
|
|
53
|
+
### 1.1.20 is deprecated on npm
|
|
54
|
+
|
|
55
|
+
Its published build is marked deprecated with a message pointing at this release. `latest` was rolled back to 1.1.19 the
|
|
56
|
+
moment the breakage was confirmed, so a fresh `npm install -g` never received the broken build.
|
|
57
|
+
|
|
58
|
+
### Verified
|
|
59
|
+
|
|
60
|
+
CI green on this commit, including `windows-latest / node 20`. Gate GREEN twice, 15 of 15. 2502 tests / 2488 pass / 0 fail.
|
|
61
|
+
|
|
62
|
+
## [1.1.20] — 2026-10-18 (withdrawn)
|
|
6
63
|
|
|
7
64
|
**The developer declares this release STABLE.** The fleet panel now shows what each machine's owner actually typed into DSH.
|
|
8
65
|
|
|
@@ -38,7 +95,14 @@ here rather than implied by a green gate.
|
|
|
38
95
|
|
|
39
96
|
### Verified
|
|
40
97
|
|
|
41
|
-
|
|
98
|
+
### The panel shows a machine, not a UUID
|
|
99
|
+
|
|
100
|
+
The first working beta displayed `d89d5817…` as the sender of every message. The message carries a `machine_id`; a name is
|
|
101
|
+
not something a sender proves to the relay, so it was never stored. The panel now reads the same roster the target control
|
|
102
|
+
reads (`readState`, once per open) and shows **name · platform**, or **name · 本机** for this machine. A machine that has no
|
|
103
|
+
name yet is shown by a short id and is the only case where an id appears at all.
|
|
104
|
+
|
|
105
|
+
Gate GREEN twice, 15 of 15. 2507 tests / 2493 pass / 0 fail / 14 skip.
|
|
42
106
|
|
|
43
107
|
## [1.1.19] — 2026-10-18
|
|
44
108
|
|
package/codex/mcp-servers.toml
CHANGED
package/lib/client.js
CHANGED
|
@@ -612,6 +612,8 @@ window.__ModuleLoader__.load({
|
|
|
612
612
|
const [open, setOpen] = React.useState(false);
|
|
613
613
|
const [messages, setMessages] = React.useState([]);
|
|
614
614
|
const [usage, setUsage] = React.useState(null);
|
|
615
|
+
const [localMachineName, setLocalMachineName] = React.useState(null);
|
|
616
|
+
const [roster, setRoster] = React.useState(null);
|
|
615
617
|
const [text, setText] = React.useState('');
|
|
616
618
|
const [error, setError] = React.useState(null);
|
|
617
619
|
const [busy, setBusy] = React.useState(false);
|
|
@@ -626,6 +628,7 @@ window.__ModuleLoader__.load({
|
|
|
626
628
|
if (result.ok) {
|
|
627
629
|
setMessages(Array.isArray(result.state.messages) ? result.state.messages : []);
|
|
628
630
|
setLocalMachineId(typeof result.state.local_machine_id === 'string' ? result.state.local_machine_id : null);
|
|
631
|
+
setLocalMachineName(typeof result.state.local_machine_name === 'string' && result.state.local_machine_name !== '' ? result.state.local_machine_name : null);
|
|
629
632
|
setUsage(result.state.usage ?? null);
|
|
630
633
|
setError(null);
|
|
631
634
|
} else {
|
|
@@ -644,6 +647,19 @@ window.__ModuleLoader__.load({
|
|
|
644
647
|
};
|
|
645
648
|
}, [open, load]);
|
|
646
649
|
|
|
650
|
+
// The chat messages carry a machine id, not a name: the relay stores what a sender proves, and a name is
|
|
651
|
+
// not one of those proofs. The roster the target control already reads is where names live, so the panel
|
|
652
|
+
// borrows it once per open instead of inventing a label from the id.
|
|
653
|
+
React.useEffect(() => {
|
|
654
|
+
if (!open || typeof sessionId !== 'string' || sessionId === '') return undefined;
|
|
655
|
+
const controller = new AbortController();
|
|
656
|
+
readState(sessionId, controller.signal).then((result) => {
|
|
657
|
+
if (controller.signal.aborted) return;
|
|
658
|
+
setRoster(result.ok ? result.state : null);
|
|
659
|
+
});
|
|
660
|
+
return () => controller.abort();
|
|
661
|
+
}, [open, sessionId]);
|
|
662
|
+
|
|
647
663
|
const send = React.useCallback(async () => {
|
|
648
664
|
const value = text.trim();
|
|
649
665
|
if (value === '' || busy) return undefined;
|
|
@@ -670,8 +686,18 @@ window.__ModuleLoader__.load({
|
|
|
670
686
|
const disabled = typeof sessionId !== 'string' || sessionId === '';
|
|
671
687
|
const usageText = usage === null ? null : `${Math.round((usage.used_24h_bytes ?? 0) / 1024)} KiB/24h · ${usage.messages ?? 0} 条`;
|
|
672
688
|
const machineLabel = (message) => {
|
|
673
|
-
const id = typeof message?.machine_id === 'string' ? message.machine_id :
|
|
674
|
-
|
|
689
|
+
const id = typeof message?.machine_id === 'string' ? message.machine_id : null;
|
|
690
|
+
if (id === null) return '?';
|
|
691
|
+
const known = Array.isArray(roster?.machines)
|
|
692
|
+
? roster.machines.find((machine) => machine.machine_id === id) ?? null
|
|
693
|
+
: null;
|
|
694
|
+
const mine = id === localMachineId;
|
|
695
|
+
const name = known?.name ?? message?.machine_name ?? (mine ? localMachineName : null);
|
|
696
|
+
const detail = mine ? '本机' : (known?.os ?? null);
|
|
697
|
+
// A machine with no name yet is shown by a short id, never by a full UUID, and the detail says which of the
|
|
698
|
+
// two it is -- "unknown" next to a UUID is what made the panel look like it was leaking identifiers.
|
|
699
|
+
const shown = name ?? (id.length > 12 ? `${id.slice(0, 8)}…` : id);
|
|
700
|
+
return detail === null ? shown : `${shown} · ${detail}`;
|
|
675
701
|
};
|
|
676
702
|
const timeLabel = (message) => (Number.isFinite(message?.at_ms) ? new Date(message.at_ms).toLocaleTimeString() : '');
|
|
677
703
|
|
package/package.json
CHANGED
|
@@ -1,214 +1,218 @@
|
|
|
1
|
-
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
"
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
"
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
"
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
"
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
"
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
"
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
"
|
|
211
|
-
"
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
+
"version": "1.1.22",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"w2m-localside": "bin/w2m-localside.mjs",
|
|
9
|
+
"w2m-mcp": "bin/w2m-mcp.mjs",
|
|
10
|
+
"w2m-rabbit": "bin/w2m-rabbit.mjs",
|
|
11
|
+
"w2m-stun": "bin/w2m-stun.mjs"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./lib/index.js",
|
|
15
|
+
"./tools": "./lib/tools.js",
|
|
16
|
+
"./client": "./lib/client.js",
|
|
17
|
+
"./rabbit": "./bin/w2m-rabbit.mjs",
|
|
18
|
+
"./localside": "./bin/w2m-localside.mjs",
|
|
19
|
+
"./stun": "./bin/w2m-stun.mjs",
|
|
20
|
+
"./package.json": "./package.json",
|
|
21
|
+
"./gpw": "./lib/gpw.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"mcp.json",
|
|
25
|
+
"plugin.json",
|
|
26
|
+
"lib",
|
|
27
|
+
"src",
|
|
28
|
+
"bin",
|
|
29
|
+
"deploy",
|
|
30
|
+
"docs",
|
|
31
|
+
"skills",
|
|
32
|
+
"cordis.patch.yml",
|
|
33
|
+
"README.md",
|
|
34
|
+
"CHANGELOG.md",
|
|
35
|
+
"HANDOFF.md",
|
|
36
|
+
"ROADMAP.md",
|
|
37
|
+
"FLEET-RULES.md",
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"PROTOCOL.md",
|
|
40
|
+
"PROTOCOL-v0.1.2.md",
|
|
41
|
+
"PROTOCOL-v0.3.0.md",
|
|
42
|
+
"PROTOCOL-v0.3.9.md",
|
|
43
|
+
"PROTOCOL-v0.4.0.md",
|
|
44
|
+
"TROUBLESHOOTING.md",
|
|
45
|
+
"SECURITY.md",
|
|
46
|
+
"CONTRIBUTING.md",
|
|
47
|
+
".claude-plugin",
|
|
48
|
+
"codex"
|
|
49
|
+
],
|
|
50
|
+
"dsh": {
|
|
51
|
+
"plugin": true,
|
|
52
|
+
"kind": "server",
|
|
53
|
+
"bundle": {
|
|
54
|
+
"patch": "cordis.patch.yml"
|
|
55
|
+
},
|
|
56
|
+
"client": {
|
|
57
|
+
"platform": "web"
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"disclosure": {
|
|
61
|
+
"cloud": true,
|
|
62
|
+
"network": [
|
|
63
|
+
"http://202.182.123.154:8787",
|
|
64
|
+
"202.182.123.154:3478",
|
|
65
|
+
"stun.cloudflare.com:3478",
|
|
66
|
+
"stun.l.google.com:19302",
|
|
67
|
+
"stun.nextcloud.com:443",
|
|
68
|
+
"ilinkai.weixin.qq.com:443",
|
|
69
|
+
"weixin.qq.com:443",
|
|
70
|
+
"api.weixin.qq.com:443"
|
|
71
|
+
],
|
|
72
|
+
"offlineMode": false,
|
|
73
|
+
"apiKeys": [
|
|
74
|
+
{
|
|
75
|
+
"env": "W2M_OPERATOR_TOKEN",
|
|
76
|
+
"storage": "profile-config"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"env": null,
|
|
80
|
+
"storage": "file-0600"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"env": null,
|
|
84
|
+
"storage": "profile-config"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"env": null,
|
|
88
|
+
"storage": "im-bot-token"
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
"permissions": {
|
|
92
|
+
"network": "the configured relay (default: the shared server above), STUN servers, and UDP to the peer machines. The optional chatops channels reach one instant-messaging platform each, and only when that channel is named in `channels` and the host has supplied the transport: the WeChat channel reaches ilinkai.weixin.qq.com and sends the bot token as a bearer credential, and both that host and the base url in a login response are checked against an allow-list before a token is sent anywhere",
|
|
93
|
+
"filesystem": "read/write inside the project directory and DSH_HOME; the agent writes its spool and state under --state; the chatops file fence reads inside `projectRoot` only, after resolving symlinks",
|
|
94
|
+
"env": [
|
|
95
|
+
"W2M_*"
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
"jurisdiction": [
|
|
99
|
+
"CN",
|
|
100
|
+
"JP"
|
|
101
|
+
],
|
|
102
|
+
"retention": "server"
|
|
103
|
+
},
|
|
104
|
+
"engines": {
|
|
105
|
+
"node": "\u003e=20.19.0"
|
|
106
|
+
},
|
|
107
|
+
"dependencies": {
|
|
108
|
+
|
|
109
|
+
},
|
|
110
|
+
"devDependencies": {
|
|
111
|
+
"@eslint/js": "^10.0.1",
|
|
112
|
+
"eslint": "^10.12.0",
|
|
113
|
+
"globals": "^17.13.0",
|
|
114
|
+
"prettier": "^3.9.9"
|
|
115
|
+
},
|
|
116
|
+
"peerDependencies": {
|
|
117
|
+
"@deepseek-ai/dsh-tools": "*"
|
|
118
|
+
},
|
|
119
|
+
"peerDependenciesMeta": {
|
|
120
|
+
"@deepseek-ai/dsh-tools": {
|
|
121
|
+
"optional": true
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
"scripts": {
|
|
125
|
+
"format": "prettier --write .",
|
|
126
|
+
"format:check": "prettier --check .",
|
|
127
|
+
"layer-check": "node scripts/layer-check.mjs",
|
|
128
|
+
"lint": "eslint .",
|
|
129
|
+
"lint:fix": "eslint . --fix",
|
|
130
|
+
"orphans": "node --test --test-force-exit test/accepted-orphans.test.mjs",
|
|
131
|
+
"pack": "node scripts/pack.mjs",
|
|
132
|
+
"reachability": "node scripts/reachability-check.mjs",
|
|
133
|
+
"reachability:classify": "node scripts/reachability-classify.mjs",
|
|
134
|
+
"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",
|
|
135
|
+
"test:acceptance-doc": "node --test --test-force-exit test/acceptance-doc.test.mjs",
|
|
136
|
+
"test:account-identity": "node --test --test-force-exit test/account-identity.test.mjs",
|
|
137
|
+
"test:account-join": "node --test --test-force-exit test/account-join.test.mjs",
|
|
138
|
+
"test:admission": "node --test --test-force-exit test/admission.test.mjs",
|
|
139
|
+
"test:admission-routes": "node --test --test-force-exit test/admission-routes.test.mjs",
|
|
140
|
+
"test:agent": "node --test --test-force-exit test/agent.test.mjs test/memory.test.mjs",
|
|
141
|
+
"test:all": "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/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.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-agent.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/p2p-crypto.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",
|
|
142
|
+
"test:approval-gate": "node --test --test-force-exit test/approval-gate.test.mjs",
|
|
143
|
+
"test:approval-index": "node --test --test-force-exit test/approval-index.test.mjs",
|
|
144
|
+
"test:authority": "node --test --test-force-exit test/authority.test.mjs",
|
|
145
|
+
"test:authority-choice": "node --test --test-force-exit test/authority-choice.test.mjs",
|
|
146
|
+
"test:blake2s": "node --test --test-force-exit test/blake2s.test.mjs",
|
|
147
|
+
"test:blocks": "node --test --test-force-exit test/blocks.test.mjs",
|
|
148
|
+
"test:council": "node --test --test-force-exit test/council.test.mjs",
|
|
149
|
+
"test:council-session": "node --test --test-force-exit test/council-session.test.mjs",
|
|
150
|
+
"test:crossnetwork": "node --test --test-force-exit test/crossnetwork.test.mjs",
|
|
151
|
+
"test:crypto-blake2s": "node --test --test-force-exit test/crypto-blake2s.test.mjs",
|
|
152
|
+
"test:e2e": "node --test --test-force-exit test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/p2p-agent.test.mjs",
|
|
153
|
+
"test:exec-env": "node --test --test-force-exit test/exec-env-injection.test.mjs",
|
|
154
|
+
"test:exit-channel": "node --test --test-force-exit test/exit-channel.test.mjs",
|
|
155
|
+
"test:field-authority": "node --test --test-force-exit test/field-authority.test.mjs",
|
|
156
|
+
"test:fleet-chat-identity": "node --test --test-force-exit test/fleet-chat-identity.test.mjs",
|
|
157
|
+
"test:fleet-concurrency": "node --test --test-force-exit test/fleet-concurrency.test.mjs",
|
|
158
|
+
"test:fleet-memory-e2e": "node --test --test-force-exit test/fleet-memory-e2e.test.mjs",
|
|
159
|
+
"test:fleet-merge": "node --test --test-force-exit test/fleet-merge.test.mjs",
|
|
160
|
+
"test:fleet-merge-race": "node --test --test-force-exit test/fleet-merge-race.test.mjs",
|
|
161
|
+
"test:fleet-messages": "node --test --test-force-exit test/fleet-messages.test.mjs",
|
|
162
|
+
"test:fleet-messages-http": "node --test --test-force-exit test/fleet-messages-http.test.mjs",
|
|
163
|
+
"test:fleet-scale": "node --test --test-force-exit test/fleet-scale.test.mjs",
|
|
164
|
+
"test:fleet-state": "node --test --test-force-exit test/fleet-state.test.mjs",
|
|
165
|
+
"test:fleet-sync": "node --test --test-force-exit test/fleet-sync.test.mjs",
|
|
166
|
+
"test:health": "node --test --test-force-exit test/health.test.mjs",
|
|
167
|
+
"test:identity-reader": "node --test --test-force-exit test/identity-reader.test.mjs",
|
|
168
|
+
"test:installed-version": "node --test --test-force-exit test/installed-version.test.mjs",
|
|
169
|
+
"test:join-challenge": "node --test --test-force-exit test/join-challenge.test.mjs",
|
|
170
|
+
"test:join-degradation": "node --test --test-force-exit test/join-degradation.test.mjs",
|
|
171
|
+
"test:layers": "node --test --test-force-exit test/layer-model.test.mjs",
|
|
172
|
+
"test:log-bounds": "node --test --test-force-exit test/log-bounds.test.mjs",
|
|
173
|
+
"test:memory": "node --test --test-force-exit test/memory.test.mjs",
|
|
174
|
+
"test:message-notify": "node --test --test-force-exit test/message-notify.test.mjs",
|
|
175
|
+
"test:message-shards": "node --test --test-force-exit test/message-shards.test.mjs",
|
|
176
|
+
"test:p2p": "node --test --test-force-exit test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/stun-server.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs",
|
|
177
|
+
"test:packet": "node --test --test-force-exit test/machine-test-packet.test.mjs",
|
|
178
|
+
"test:permission": "node --test --test-force-exit test/permission.test.mjs",
|
|
179
|
+
"test:portable": "node --test --test-force-exit test/portable-plugins.test.mjs",
|
|
180
|
+
"test:reachability": "node --test --test-force-exit test/reachability-check.test.mjs",
|
|
181
|
+
"test:relay": "node --test --test-force-exit test/relay.test.mjs",
|
|
182
|
+
"test:session-readonly": "node --test --test-force-exit test/session-readonly.test.mjs",
|
|
183
|
+
"test:session-sync": "node --test --test-force-exit test/session-sync.test.mjs",
|
|
184
|
+
"test:session-watch": "node --test --test-force-exit test/session-watch.test.mjs",
|
|
185
|
+
"test:storage": "node --test --test-force-exit test/storage.test.mjs",
|
|
186
|
+
"test:test-report": "node --test --test-force-exit test/test-report.test.mjs",
|
|
187
|
+
"test:tools": "node --test --test-force-exit test/tools.test.mjs",
|
|
188
|
+
"test:update-channels": "node --test --test-force-exit test/update-channels.test.mjs",
|
|
189
|
+
"test:update-prerelease": "node --test --test-force-exit test/update-prerelease.test.mjs",
|
|
190
|
+
"verify": "node scripts/check-entrypoints.mjs \u0026\u0026 node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/tools.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/p2p-node.test.mjs test/p2p-agent.test.mjs test/client-bundle.test.mjs test/loader-entry.test.mjs",
|
|
191
|
+
"verify:join": "node scripts/verify-join.mjs"
|
|
192
|
+
},
|
|
193
|
+
"keywords": [
|
|
194
|
+
"dsh-bundle",
|
|
195
|
+
"dsh-plugin",
|
|
196
|
+
"deepseek-harness",
|
|
197
|
+
"deepseek",
|
|
198
|
+
"windows2macos",
|
|
199
|
+
"w2m",
|
|
200
|
+
"cross-platform",
|
|
201
|
+
"multi-machine",
|
|
202
|
+
"orchestration",
|
|
203
|
+
"replicate",
|
|
204
|
+
"sharding"
|
|
205
|
+
],
|
|
206
|
+
"repository": {
|
|
207
|
+
"type": "git",
|
|
208
|
+
"url": "git+https://github.com/TwinsEarth/dsh-windows2macos.git"
|
|
209
|
+
},
|
|
210
|
+
"homepage": "https://github.com/TwinsEarth/dsh-windows2macos",
|
|
211
|
+
"bugs": {
|
|
212
|
+
"url": "https://github.com/TwinsEarth/dsh-windows2macos/issues"
|
|
213
|
+
},
|
|
214
|
+
"publishConfig": {
|
|
215
|
+
"access": "public"
|
|
216
|
+
},
|
|
217
|
+
"license": "MIT"
|
|
218
|
+
}
|
package/plugin.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
|
+
"name": "twinsearth-w2m-dsh",
|
|
4
|
+
"version": "1.1.22",
|
|
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
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"agent-skills",
|
|
9
|
+
"fleet",
|
|
10
|
+
"multi-machine",
|
|
11
|
+
"w2m",
|
|
12
|
+
"dsh"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -65,22 +65,35 @@ export function registerChatRoutes(ctx, deps) {
|
|
|
65
65
|
* Resolved once. The account material is hashed with a domain label first: it is the fleet key, which is secret, but
|
|
66
66
|
* hashing keeps the conversation key from ever being usable as a joining secret.
|
|
67
67
|
*/
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Which machine this is and which conversation the project belongs to.
|
|
70
|
+
*
|
|
71
|
+
* Resolved lazily and NOT cached while it is incomplete: the fleet key may land after the first request (the account
|
|
72
|
+
* join runs at stack start), and a panel opened before it did would otherwise keep a "no identity" answer for the
|
|
73
|
+
* lifetime of the process -- which is exactly the bug this replaces.
|
|
74
|
+
*/
|
|
75
|
+
let resolved = null;
|
|
76
|
+
const identityOf = async () => {
|
|
77
|
+
if (resolved !== null && resolved.error === null) return resolved;
|
|
70
78
|
const local = typeof deps.localIdentity === 'function' ? await deps.localIdentity() : null;
|
|
71
|
-
const material = typeof deps.accountMaterial === 'function' ? deps.accountMaterial() : null;
|
|
72
79
|
const machineId = typeof local?.machine_id === 'string' && local.machine_id !== '' ? local.machine_id : null;
|
|
73
|
-
if (machineId === null
|
|
74
|
-
|
|
80
|
+
if (machineId === null) {
|
|
81
|
+
// Nothing can be keyed without a machine, and inventing one would put unrelated hosts in one conversation.
|
|
82
|
+
return { error: 'CHAT_NO_IDENTITY', machineId: null, machineName: null, conversationKey: null, source: null };
|
|
75
83
|
}
|
|
76
|
-
const
|
|
77
|
-
|
|
84
|
+
const material = typeof deps.accountMaterial === 'function' ? deps.accountMaterial() : null;
|
|
85
|
+
const basis = conversationBasis({ machineId, accountMaterial: material });
|
|
86
|
+
const fingerprint = createHash('sha256')
|
|
87
|
+
.update('w2m-conversation-basis-v1').update('\\u0000').update(basis.basis).digest('hex');
|
|
88
|
+
resolved = {
|
|
78
89
|
error: null,
|
|
79
90
|
machineId,
|
|
80
91
|
machineName: typeof local?.machine_name === 'string' ? local.machine_name : null,
|
|
81
92
|
conversationKey: deriveConversationKey(fingerprint, cwd),
|
|
93
|
+
source: basis.source,
|
|
82
94
|
};
|
|
83
|
-
|
|
95
|
+
return resolved;
|
|
96
|
+
};
|
|
84
97
|
|
|
85
98
|
/** Post one batch into the relay using the machine's device token, exactly as the browser route does. */
|
|
86
99
|
const postToRelay = async (identity, payload) => {
|
|
@@ -154,13 +167,13 @@ export function registerChatRoutes(ctx, deps) {
|
|
|
154
167
|
sessionId = sessionIdOf(body?.session_id);
|
|
155
168
|
text = typeof body?.text === 'string' ? body.text : null;
|
|
156
169
|
}
|
|
157
|
-
const identity = await identityOf();
|
|
158
|
-
if (identity.error !== null) {
|
|
159
|
-
return refuse(503, identity.error, 'this machine has no fleet-chat identity yet', 'pair the machine and let it derive the fleet key');
|
|
160
|
-
}
|
|
161
|
-
// v1.1.20: the browser's session id is per-machine and no other machine can compute it, so the wire key is the
|
|
162
|
-
// conversation key (account + project directory). The browser's value is accepted and then replaced.
|
|
163
|
-
sessionId = identity.conversationKey;
|
|
170
|
+
const identity = await identityOf();
|
|
171
|
+
if (identity.error !== null) {
|
|
172
|
+
return refuse(503, identity.error, 'this machine has no fleet-chat identity yet', 'pair the machine and let it derive the fleet key');
|
|
173
|
+
}
|
|
174
|
+
// v1.1.20: the browser's session id is per-machine and no other machine can compute it, so the wire key is the
|
|
175
|
+
// conversation key (account + project directory). The browser's value is accepted and then replaced.
|
|
176
|
+
sessionId = identity.conversationKey;
|
|
164
177
|
if (sessionId === '') return refuse(400, 'CHAT_SESSION_ID_REQUIRED', '`session_id` is required', 'chat is scoped to one conversation');
|
|
165
178
|
if (sessionId.length > MAX_SESSION_ID_LENGTH) {
|
|
166
179
|
return refuse(400, 'CHAT_SESSION_ID_TOO_LONG', `\`session_id\` must be at most ${MAX_SESSION_ID_LENGTH} characters`, 'send the id the host gave this session');
|
|
@@ -192,7 +205,7 @@ export function registerChatRoutes(ctx, deps) {
|
|
|
192
205
|
const message = listed?.json?.error?.message ?? listed?.error?.message ?? `the relay answered HTTP ${listed?.status ?? 0}`;
|
|
193
206
|
return refuse(listed?.status === 404 ? 502 : 502, code, message, 'the relay must run a version with /v1/fleet/messages (>= 1.1.17) and be restarted');
|
|
194
207
|
}
|
|
195
|
-
return Response.json({ ...listed.json, ok: true, local_machine_id: identity.machineId, local_machine_name: identity.machineName, conversation_key: sessionId }, { headers: NO_STORE });
|
|
208
|
+
return Response.json({ ...listed.json, ok: true, local_machine_id: identity.machineId, local_machine_name: identity.machineName, conversation_key: sessionId, conversation_source: identity.source }, { headers: NO_STORE });
|
|
196
209
|
}
|
|
197
210
|
const posted = await deps.request({ rabbitUrl, token, method: 'POST', pathname: '/v1/fleet/messages', body: { session_id: sessionId, text: text ?? '', machine_id: identity.machineId, machine_name: identity.machineName, origin: 'chat' }, signal: request.signal });
|
|
198
211
|
if (posted?.ok !== true) {
|
|
@@ -219,3 +232,22 @@ export function registerChatRoutes(ctx, deps) {
|
|
|
219
232
|
});
|
|
220
233
|
return status;
|
|
221
234
|
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The material a conversation is keyed from, and which scope it represents.
|
|
238
|
+
*
|
|
239
|
+
* The account key is the right basis -- two machines on the same account and the same project are one conversation. But
|
|
240
|
+
* the account key is derived asynchronously at stack start, and a panel that cannot answer at all is worse than a panel
|
|
241
|
+
* that answers for one machine until the join lands. So the machine id is the documented fallback, and the answer says
|
|
242
|
+
* which basis it used rather than leaving the caller to guess.
|
|
243
|
+
*
|
|
244
|
+
* @param {{machineId?: string|null, accountMaterial?: string|null}} input
|
|
245
|
+
* @returns {{basis: string, source: 'account'|'machine'}|null} null when there is no machine to key on
|
|
246
|
+
*/
|
|
247
|
+
export function conversationBasis({ machineId, accountMaterial } = {}) {
|
|
248
|
+
const machine = typeof machineId === 'string' && machineId !== '' ? machineId : null;
|
|
249
|
+
if (machine === null) return null;
|
|
250
|
+
const account = typeof accountMaterial === 'string' && accountMaterial.trim() !== '' ? accountMaterial.trim() : null;
|
|
251
|
+
if (account !== null) return { basis: 'account:' + account, source: 'account' };
|
|
252
|
+
return { basis: 'machine:' + machine, source: 'machine' };
|
|
253
|
+
}
|
|
@@ -19,7 +19,31 @@
|
|
|
19
19
|
* ask for them.
|
|
20
20
|
*/
|
|
21
21
|
import { createHash } from 'node:crypto';
|
|
22
|
-
import {
|
|
22
|
+
import { createRequire } from 'node:module';
|
|
23
|
+
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The zstd API, or null on a runtime that does not have it.
|
|
28
|
+
*
|
|
29
|
+
* It is looked up lazily and through \`require\` on purpose: a static \`import { zstdDecompressSync } from 'node:zlib'\`
|
|
30
|
+
* makes the WHOLE module fail to link on Node 20, which is where part of the support matrix runs, and the failure is not
|
|
31
|
+
* "this feature is unavailable" -- it is "the plugin does not load". A missing decoder must degrade to an empty read.
|
|
32
|
+
*/
|
|
33
|
+
let zstdModule = null;
|
|
34
|
+
function zstdApi() {
|
|
35
|
+
if (zstdModule !== null) return zstdModule;
|
|
36
|
+
try {
|
|
37
|
+
const loaded = require('node:zlib');
|
|
38
|
+
zstdModule = typeof loaded.zstdDecompressSync === 'function' ? loaded : { zstdDecompressSync: null };
|
|
39
|
+
} catch { zstdModule = { zstdDecompressSync: null }; }
|
|
40
|
+
return zstdModule;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Whether this runtime can decode a session transcript at all (Node >= 22.15 exposes the zstd API). */
|
|
44
|
+
export function zstdAvailable() {
|
|
45
|
+
return typeof zstdApi().zstdDecompressSync === 'function';
|
|
46
|
+
}
|
|
23
47
|
|
|
24
48
|
/** The zstd frame magic number, little-endian bytes 28 B5 2F FD. */
|
|
25
49
|
const ZSTD_MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]);
|
|
@@ -47,9 +71,11 @@ export function splitZstdFrames(buffer) {
|
|
|
47
71
|
*/
|
|
48
72
|
export function decodeTranscript(buffer) {
|
|
49
73
|
const events = [];
|
|
74
|
+
const decode = zstdApi().zstdDecompressSync;
|
|
75
|
+
if (typeof decode !== 'function') return events; // no decoder on this runtime: nothing to read, nothing to crash
|
|
50
76
|
for (const frame of splitZstdFrames(buffer)) {
|
|
51
77
|
let text;
|
|
52
|
-
try { text =
|
|
78
|
+
try { text = decode(frame).toString('utf8'); } catch { continue; }
|
|
53
79
|
for (const line of text.split('\n')) {
|
|
54
80
|
if (line === '') continue;
|
|
55
81
|
try { events.push(JSON.parse(line)); } catch { /* a partial trailing line is expected in a live file */ }
|
package/src/plugin/tools.mjs
CHANGED
|
@@ -220,7 +220,7 @@ const MAX_STAGES = 16;
|
|
|
220
220
|
* `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
|
|
221
221
|
* overrides it at runtime for tests and packagers.
|
|
222
222
|
*/
|
|
223
|
-
const PLUGIN_VERSION = '1.1.
|
|
223
|
+
const PLUGIN_VERSION = '1.1.22';
|
|
224
224
|
|
|
225
225
|
/** How long any single HTTP call may take before it is aborted, in milliseconds. */
|
|
226
226
|
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|
|
@@ -3325,8 +3325,11 @@ export async function apply(ctx, config = {}) {
|
|
|
3325
3325
|
* cannot disagree about when a missing identity becomes a stale one.
|
|
3326
3326
|
*/
|
|
3327
3327
|
const localIdentity = createLocalIdentity(async () => {
|
|
3328
|
-
|
|
3329
|
-
|
|
3328
|
+
// readLocalIdentity knows the <DSH_HOME>/xclient/device.json fallback; readDevice() returns empty when
|
|
3329
|
+
// `stateDir` is unset, which is the documented default layout -- and that made every consumer of this helper
|
|
3330
|
+
// answer "no identity" on a paired machine (the fleet-chat panel said so verbatim).
|
|
3331
|
+
const identity = await readLocalIdentity(cfg);
|
|
3332
|
+
return { machine_id: identity.machine_id, machine_name: identity.machine_name, machineName: cfg.machineName };
|
|
3330
3333
|
});
|
|
3331
3334
|
|
|
3332
3335
|
/**
|
|
@@ -3415,10 +3418,10 @@ export async function apply(ctx, config = {}) {
|
|
|
3415
3418
|
// Both are already resolved in this scope for the target control; passing them here is what lets the route key
|
|
3416
3419
|
// messages by account+project instead of by a per-machine session id no other machine can compute.
|
|
3417
3420
|
localIdentity,
|
|
3418
|
-
// The account material is the fleet key
|
|
3419
|
-
// and
|
|
3420
|
-
//
|
|
3421
|
-
accountMaterial: () =>
|
|
3421
|
+
// The account material is the fleet key the local stack resolved (config first, then the account join). NOT the
|
|
3422
|
+
// account code: that is written to logs, and a conversation key derivable from a printed value would let anyone in.
|
|
3423
|
+
// Null until the join lands; the route then keeps the conversation scoped to this machine instead of refusing.
|
|
3424
|
+
accountMaterial: () => resolvedFleetKey.value,
|
|
3422
3425
|
});
|
|
3423
3426
|
if (chatRoutes.mounted !== true) {
|
|
3424
3427
|
// Named rather than silent: a composition without the fetch registry loses the chat panel while every tool still
|
|
@@ -6561,6 +6564,16 @@ function sleepWithSignal(ms, signal) {
|
|
|
6561
6564
|
* @param {ReturnType<typeof readConfig>} cfg
|
|
6562
6565
|
* @returns {Promise<() => Promise<void>>} A disposer that stops the agent and closes the relay.
|
|
6563
6566
|
*/
|
|
6567
|
+
/**
|
|
6568
|
+
* The account material the local stack resolved, readable from the rest of the plugin.
|
|
6569
|
+
*
|
|
6570
|
+
* The fleet key is derived inside startLocalStack (it needs the account join), but the composer's chat route is
|
|
6571
|
+
* registered in apply(), one scope above: without this hand-off the route can only see `cfg.fleetKey`, which is null on
|
|
6572
|
+
* a machine that joined by account -- and the panel then refused every request with "no fleet-chat identity yet".
|
|
6573
|
+
* A holder rather than a parameter because the route is mounted before the stack starts, and reads lazily.
|
|
6574
|
+
*/
|
|
6575
|
+
const resolvedFleetKey = { value: null, source: null, at: 0 };
|
|
6576
|
+
|
|
6564
6577
|
async function startLocalStack(ctx, cfg) {
|
|
6565
6578
|
const relayUrl = new URL('../relay/server.mjs', import.meta.url);
|
|
6566
6579
|
const agentUrl = new URL('../agent/agent.mjs', import.meta.url);
|
|
@@ -6816,6 +6829,9 @@ async function startLocalStack(ctx, cfg) {
|
|
|
6816
6829
|
}
|
|
6817
6830
|
|
|
6818
6831
|
let { fleetKey, fleetKeySource } = await deriveFleetKeyOnce();
|
|
6832
|
+
resolvedFleetKey.value = fleetKey;
|
|
6833
|
+
resolvedFleetKey.source = fleetKeySource;
|
|
6834
|
+
resolvedFleetKey.at = Date.now();
|
|
6819
6835
|
|
|
6820
6836
|
/**
|
|
6821
6837
|
* Name what actually went wrong, including the shapes that used to say nothing.
|