@mmmbuto/nexuscrew 0.8.52-rc.9 → 0.8.53

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/CHANGELOG.md CHANGED
@@ -4,6 +4,346 @@ All notable changes to NexusCrew are tracked here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.8.53 — 2026-08-07 — "Coming Back Up, and Saying So"
8
+
9
+ Automatic updates are on by default and check every six hours, so a node
10
+ installs a new release and restarts itself without anyone watching. That makes
11
+ the restart path the most consequential code in the product, and this release
12
+ is about what happens when it doesn't work: today a node was measured staying
13
+ down for over twenty minutes after a restart, with its tunnel still up, while
14
+ nothing anywhere said so.
15
+
16
+ - **A restart now confirms the service came back.** `nexuscrew restart` used to
17
+ report success as soon as the restart *command* returned — which says the
18
+ command ran, not that anything is answering. The two look identical to whoever
19
+ reads the exit code. The check already existed and was already used by two
20
+ other paths: the auto-updater waits for health and fails if it doesn't come,
21
+ and the Fleet bootstrap does the same, with a comment that literally says "a
22
+ verified restart is needed". It was missing precisely on the command a person
23
+ types by hand — the one where no other code is checking on your behalf.
24
+
25
+ - **A service that exited after a restart is brought back, once.** On a phone
26
+ there is no service manager to raise the process again: if it goes, it stays
27
+ gone. That was measured — a node stayed down for over twenty minutes after a
28
+ restart, with its reverse tunnel still up, and nothing brought it back. Now,
29
+ when the service does not answer, the port decides what happens: free means
30
+ the process is gone and it is started again *once*; still busy means something
31
+ is holding it without serving, and retrying would only hide that. Once and no
32
+ more — repeating turns a fault into a loop. On a machine where a service
33
+ manager owns the runtime nothing is started alongside it: that is the
34
+ manager's job, and a process it does not know about would race its own unit
35
+ for the port and outlive a stop.
36
+
37
+ **Why the process exited is not established.** A plausible story — that the
38
+ restart did not wait for the old process before starting the new one — turned
39
+ out to be wrong on inspection: it does wait, and has since 0.8.17. So this is
40
+ a recovery for a failure whose cause is still open, not a fix for a known one,
41
+ and it is worth knowing which of the two you are relying on.
42
+
43
+ - **"Peer unreachable" no longer covers two different failures.** With a reverse
44
+ SSH channel, a device that is not connected leaves no listener and the
45
+ connection is *refused*; a device that is connected but whose NexusCrew has
46
+ died accepts the connection and then *resets* it. Those need opposite
47
+ remedies — one is fixed on the network, the other by going to the device —
48
+ and one message sent the investigation to the wrong place half the time. It
49
+ did: four hours were spent in federation and pairing while the defect was a
50
+ service that had not come back on a phone. The two are now named, with the
51
+ port, and an error nobody recognises is still reported as before rather than
52
+ guessed into a layer.
53
+
54
+ - **The interface reloads itself after the node updates.** With automatic
55
+ updates on, a node updates and restarts while an open app keeps running the
56
+ old bundle. The only way out was closing and reopening it — the banner had to
57
+ be tapped, and because of a service-worker defect fixed in 0.8.52, tapping it
58
+ did not work either. The app now applies the new bundle by itself. Only for
59
+ the case a reload can fix: when the package on the server is newer than the
60
+ interface it serves, no amount of reloading changes that, and it is left
61
+ alone. If the mismatch survives the reload it is not retried — a reload loop
62
+ makes the app unusable, which is far worse than a banner, so the banner
63
+ remains as the fallback. What you were typing is not lost: the composer draft
64
+ was already kept across reloads, which is what made this acceptable. The
65
+ version check now also repeats about once a minute rather than running only
66
+ when the app starts — an app left open in front of someone is precisely the
67
+ case this exists for, and checking only at startup would have made it work
68
+ solely for people who had already closed and reopened it.
69
+
70
+ - **`nexuscrew autoupdate on|off|status`.** The switch already existed — a
71
+ persisted setting, on by default, with a checkbox in Settings. It was missing
72
+ from the command line, which is where you need it: when a node has updated
73
+ itself and the service did not come back, the interface is the thing you
74
+ cannot open. With the service running the command goes through the API rather
75
+ than writing the file, because writing the file would leave the running
76
+ process with the old value — the setting would read "off" while updates kept
77
+ happening on schedule, and a switch that reads off without switching anything
78
+ off is worse than no switch. If the service is up but not answering, nothing
79
+ is written at all.
80
+
81
+ - **An error now says when the tool bridge is older than the hub.** Updating
82
+ NexusCrew does not update the MCP bridge of an already running session: that
83
+ process started with the previous code and keeps it until the session is
84
+ restarted. The symptom is cruel — you install a fix, try again, and get the
85
+ *old* error, so you conclude the fix does not work and go looking where the
86
+ defect is not. The check runs only on the error path, which costs nothing in
87
+ normal use and is the only moment it helps; it cannot be cached at startup
88
+ either, since the version that changes is the hub's, and it changes while the
89
+ bridge is running.
90
+
91
+ - **A long message sent to a cell is no longer pasted and left unsent.** The
92
+ wait between the bracketed paste and the Enter was a constant, while the time
93
+ a terminal interface needs to swallow a paste grows with its size — above a
94
+ certain length the client collapses it, the Enter lands while it is still
95
+ being processed, and it is swallowed. The message then sat in the composer
96
+ while the sender got a delivery receipt. Measured on the same target eleven
97
+ minutes apart: 2900 characters were never processed for nine hours, sixty
98
+ characters were being worked on in twelve seconds. The wait now grows with the
99
+ text, and below 500 characters nothing changes — those were already reliable,
100
+ and slowing them would be a cost paid by everyone for a defect that is not
101
+ theirs. This narrows the window rather than closing it: a receipt still means
102
+ paste and Enter, not acceptance.
103
+
104
+ ## 0.8.52 — 2026-08-07 — "What a Peer May See, and What a Cell May Reach"
105
+
106
+ - **Each node now has a cryptographic identity, and it changes nothing yet.**
107
+ Every installation generates an Ed25519 key pair; the private half stays in
108
+ its own file, readable only by its owner, and never leaves the device. Public
109
+ halves are exchanged during pairing — inside the act that consumes the
110
+ one-time invite, which is the only moment at which the operator has decided,
111
+ *on both machines*, that these two nodes know each other. Binding a key
112
+ anywhere else would bind it to a channel the peer controls alone.
113
+
114
+ Nothing is gated on it. A node running an older version sends no key and
115
+ pairs exactly as before; a malformed key is ignored rather than refused,
116
+ because a fault here must never be able to stop you from pairing a device.
117
+ This is the first step of a per-node authority model, and it is deliberately
118
+ the step with no effect: what it buys is *time*. A key bound today has a
119
+ history in six months; one bound when permissions start depending on it has
120
+ none.
121
+
122
+ Pairing is the only thing that writes a key: no other path sets one, so no
123
+ key can be replaced by anything a peer merely asserts. That is the property
124
+ worth having, and it is currently guaranteed by there being no second writer
125
+ rather than by a check — the check belongs with the first path that learns a
126
+ key outside pairing, and arrives with it.
127
+
128
+ **What this does not do yet**: peers paired before this version have no key
129
+ and will not have one until they are paired again, so on an existing
130
+ installation the directory starts out empty. Keys are learned only during
131
+ pairing, and replacing one on a peer that already has it means removing that
132
+ peer and pairing it again.
133
+
134
+ - **A paired node can now be restricted to a subset of your cells.** Pairing
135
+ was all or nothing: a peer saw every cell on the hub and could act on all of
136
+ them. The permission lives in the node store of the hub that owns the cells,
137
+ never in the body of a request, and it is keyed on the cell id — which is
138
+ unique and immutable — rather than the tmux session, which is derived and
139
+ accepts non-canonical overrides. A missing field means `all`: a fail-closed
140
+ default would have silenced an entire fleet on the first upgrade without
141
+ anyone deciding anything, so narrowing stays an explicit act. `selected` with
142
+ an empty list means *no cells*, and it is a different thing from an absent
143
+ field — which is exactly why the mode is its own field instead of being
144
+ inferred from the array.
145
+
146
+ The guard sits at the head of the `/api` router rather than on each route,
147
+ because the channels a cell name leaks through are many and growing: reads
148
+ (`/cells`, `/fleet/status` — the list the remote PWA actually uses,
149
+ `/fleet/definitions`, `/sessions` including the terminal `preview`, `/decks`,
150
+ and the `records` of `/diagnostics/logs`) and actions (fleet up, down,
151
+ restart, engine, boot; `cells/send`; creating and deleting sessions; files)
152
+ pass through the same predicate. Every route declares its target in a table
153
+ instead of having one guessed, and an undeclared route that names a cell is
154
+ refused — a 403 on something legitimate is noticed and fixed, a channel
155
+ nobody sees is not. Defining cells is denied to a restricted peer, or it
156
+ would create the cell it is missing and act on that.
157
+
158
+ The WebSocket attach honours it too, and without that the rest would be
159
+ decoration: `/ws` attaches a PTY *by session name*, so a peer whose cell we
160
+ had hidden from every list could still attach by guessing `cloud-Dev`. An
161
+ out-of-scope session is treated as nonexistent — the same code as one that
162
+ really is not there, because answering "it exists but you may not" reveals
163
+ precisely what the scope hides. The scope is set from the node's sheet in the
164
+ interface and with `nexuscrew nodes cells <node> all|none|Cell1,Cell2`.
165
+
166
+ **This is not the admin/user class.** Within what it can see, a node in scope
167
+ is still trusted as its owner. Scope answers *which cells*, not *what
168
+ authority* — the second is a layer that does not exist yet.
169
+
170
+ - **A peer that is not yours does not inherit a restriction of yours.** Two
171
+ peers of the same hub, which do not know each other, stopped seeing each
172
+ other's cells. On the node that answers, the chain is [A, B, C]: the delivering
173
+ peer B is in its store, the origin A is not — and has no reason to be, since a
174
+ transitive peer arrives through a hub that was authorized to route it.
175
+ Treating the unknown as `none` looked like prudence and was a door shut in the
176
+ face of legitimate traffic. The delivering peer's restriction still applies in
177
+ full, and an origin that *is* in the store still carries its own across
178
+ multiple hops; an unknown *deliverer* stays fail-closed, because that one
179
+ spoke to us and authenticated.
180
+
181
+ - **VL micro-devices are nodes inside NexusCrew.** A bounded bridge carries a
182
+ micro-device's cells to the hub: the node appears in the Settings list and in
183
+ the sidebar, its session is readable in full width, and its commands come from
184
+ the capabilities the device declares rather than from a hardcoded list. The
185
+ hub accepts the `prompt` verb with the same 4 KiB ceiling as the device — two
186
+ different numbers would mean handing the node commands it will refuse while
187
+ returning a `submitted` that reads as "it left". Nodes are aggregated across
188
+ *all* authorized owners, not just the local one: an owner that does not answer
189
+ shows as unavailable instead of silently reading as "no nodes", every node is
190
+ tagged with the owner it came from, and a command is routed to the node's
191
+ actual owner rather than quietly to the local endpoint.
192
+
193
+ What does **not** cross the boundary: the arguments and results of tools.
194
+ They are at once the largest and the most dangerous — files, command output,
195
+ possible secrets — and they do not leave even truncated. On the hub the events
196
+ stay in memory and nothing more, one ring per node, lost on restart: the
197
+ durable copy is the journal on the device, and persisting here would extend
198
+ in time a visibility that today is only live. A gap is always reported with
199
+ its count, because a silent absence reads as "nothing happened".
200
+
201
+ - **Models can be declared in the configuration, without waiting for a
202
+ release.** The model catalogue lived in the package, so a provider publishing
203
+ a new id made it unusable until the next version — four managed profiles have
204
+ `strictModels`, and there an out-of-catalogue id is not a warning, it is a
205
+ cell that does not start. `fleet.json` now accepts `models` alongside
206
+ `engines`, persisted with them: id, the managed profile it applies to, and the
207
+ fields the client expects. The built-in catalogue remains the list of *known*
208
+ models and becomes a default rather than a wall.
209
+
210
+ Before declaring one you can ask whether it works, from the models window:
211
+ the check queries the provider's model list — which costs no tokens and
212
+ answers exactly the question asked — and replies with a closed set of
213
+ outcomes. A provider that does not expose the list gives `unverified`, never
214
+ `unknown-model`: "I don't know" must not read as "it does not exist", or the
215
+ right model would be declared nonexistent. And `unverified` is not `ok`: a
216
+ proof that was not obtained does not authorize saying it works.
217
+
218
+ Declared models travel with the backup — a round trip used to lose them, and
219
+ the engine that used them was then refused on restore — and they are written
220
+ *before* the engines, in the same mutation, because that order is what makes
221
+ the restore succeed. They also follow the engines across the federation:
222
+ `define-engine` was federated and `define-model` was not, so administering a
223
+ paired node stopped halfway, and the models window now acts on the node you
224
+ are *looking at* — testing a remote node's model against your own fleet
225
+ answers a different question than the one asked.
226
+
227
+ - **`qwen3.8-max-preview` became `qwen3.8-max` without stopping the cells that
228
+ used the old name.** The preview was promoted and the id changed; with
229
+ `strictModels` that is not a warning but a cell that will not start, and two
230
+ cells in the live configuration used the old name. The rename goes through an
231
+ alias declared in one place and applied *before* the gate, so the old name
232
+ resolves — and resolves to the new one, which means the configuration
233
+ converges by itself at the first rewrite instead of lagging behind a silent
234
+ compatibility. The alias covers declared renames, not arbitrary ids.
235
+
236
+ - **The same model with and without a tag now gets the same context window.**
237
+ `deepseek-v4-flash:0731` found itself with 180k instead of 1M after upgrading
238
+ to 0.8.51: the context map is keyed without the tag, the direct lookup missed
239
+ it, and the generic 200000 fallback took over — the cell started, worked, and
240
+ had a fifth of the context it should have. The fix is not to truncate at the
241
+ first colon, because some keys have a tag that is part of the model's identity
242
+ (`qwen3.5:397b`, `mistral-large-3:675b`) and a blind normalization would break
243
+ those two to fix this one. The exact key is tried first, the base name only
244
+ after.
245
+
246
+ - **A cell can be given a named subset of MCP servers.** Cells share one
247
+ configuration, so every cell reached every tool the operator had installed,
248
+ and there was no way to say otherwise. A cell may now declare
249
+ `mcp` with the names it is allowed, and an absent field is not an empty one —
250
+ without it nothing changes, which is what every existing cell gets. Only
251
+ *names* travel, in the process arguments:
252
+ the obvious design would generate one file per cell containing the server
253
+ *definitions*, and on a real installation some of those carry credentials in
254
+ their environment, so the secrets would have been duplicated once per cell.
255
+ Granting is done by denying the complement, because a broader deny beats a
256
+ narrower allow in the client; `mcp: []` uses the wildcard and is therefore the
257
+ exact case. The complement is enumerated from the three sources a session
258
+ really loads — the user configuration, the local scope of the cell's working
259
+ directory, and a project `.mcp.json` — and the window says so, since a server
260
+ from a source nobody enumerates would pass while the operator believed it
261
+ excluded.
262
+
263
+ - **A cell isolated by credential no longer loses its tools.** NexusCrew gives
264
+ the Claude client a private configuration directory on certain credential
265
+ profiles, which is right — it separates the keys. But the client keeps the MCP
266
+ list in that same file, and in a private profile that list is empty: measured,
267
+ 0 servers against the 8 of the main configuration. A cell there ran with no
268
+ memory, no notifications and no web access, and from outside it just looked
269
+ like a cell that does not use its tools. The private profile now points at the
270
+ main configuration for the server list, so the isolation stays on the keys
271
+ where it belongs.
272
+
273
+ - **A failed cell lookup now says which of the three things went wrong.** One
274
+ message covered every failure — cell absent, node unreachable, id unknown to
275
+ this hub — and it named the cell, so a mistyped node id was read as a missing
276
+ cell and the investigation went where the defect was not. The three cases are
277
+ now distinguished, and the third suggests copying the id from the directory
278
+ rather than retyping it. The same diagnosis was applied to the VL tools.
279
+
280
+ - **An alert to the operator now crosses the federation.** The notification
281
+ channel was born as cell → operator *on the same host*, with three local
282
+ anchors: the bridge speaks only to loopback, the event hub is a set in that
283
+ process's memory, and the push keys are files of that installation. Anyone
284
+ working from another node received nothing, while the reply reported success
285
+ because those counters count *attempts*, not deliveries. A notification now
286
+ accepts an exact target instance; with a remote one the request travels the
287
+ existing federated route and the node that owns the screen delivers with its
288
+ own subscriptions and its own keys — so a real web push, with the app closed.
289
+
290
+ - **A refusal the hub writes down nowhere does not exist.** A node could not
291
+ enable Share and the hub refused it thirty times, leaving no trace: the error
292
+ lived only in the toast on the device, which is the one place the hub's
293
+ administrator cannot look. The cause — a reverse-port grant that named another
294
+ peer's port — surfaced only by reading sshd's log as root. The hub now records
295
+ what it refused and why, with the port it attempted, because "channel not
296
+ ready" does not say where to look. The record lives in the diagnostic buffer
297
+ in memory, so it is readable while the service runs and is lost on restart —
298
+ which is enough for the case it was written for, a refusal repeating now, and
299
+ is not a durable audit trail. Related: `nodes test` with no argument now
300
+ tests every direct peer in parallel and ends with the line that existed
301
+ nowhere — which nodes report themselves as shared while their reverse channel
302
+ does not answer.
303
+
304
+ - **Minting a pairing invite works again on a paired installation.** After
305
+ 0.8.51 moved minting to the local installation, the button could not succeed
306
+ on a hub-paired node: the interface still implicitly picked the first outbound
307
+ peer and delegated the minting to it — a path that now answers 404 — and the
308
+ local form disappeared entirely whenever a hub existed, so no route was left.
309
+ An invite is now always minted for the installation that issues it, and the
310
+ panel says which one, which was half the reason the previous behaviour went
311
+ unnoticed.
312
+
313
+ - **The "new version" banner can be dismissed.** The update button messaged a
314
+ waiting service worker and waited for it to take over, but the worker
315
+ registered no message listener: the message fell into nothing, the fallback
316
+ reload fired, the worker stayed waiting, and on reload the banner came back.
317
+ Permanent by construction, with a button that could not turn it off. Anyone
318
+ with a stuck worker needs to do nothing — the new one activates itself.
319
+
320
+ - **Mobile fixes.** The keyboard closed on every letter typed into a node's
321
+ prompt field: the sheet's focus effect was armed on a callback the parents
322
+ recreate on each poll, so every polling round stole the focus back from under
323
+ your thumb. `prompt` is now a field rather than a trigger — the interface
324
+ built its buttons from the declared capabilities and sent every verb with no
325
+ arguments, which the device correctly refused. The models window can be left
326
+ without saving: it had only a save button, and while it also closes with
327
+ Escape or a click outside, a phone has no Escape and the backdrop may be out
328
+ of reach. VL cells no longer appear twice in the switcher, and the VL event
329
+ list is idempotent by sequence number — on a slow link the poll period expired
330
+ before the answer, the next tick started from the old cursor, and identical
331
+ rows piled up.
332
+
333
+ - **Interface language.** Three labels stayed in English whatever the language;
334
+ the cell prompt now explains what it does outside Italian too; two help texts
335
+ promised things about credentials that had stopped being true, and one
336
+ described a backup content that had changed. The end-of-turn label said
337
+ "session ended" on every reply — host, node, cell and pump were all measurably
338
+ alive.
339
+
340
+ - **The governance surface is pinned by name.** The routes that change who may
341
+ do what on a node are unreachable from the federation for an implicit reason:
342
+ they are not in the allowlist. That holds until someone adds a line, and on
343
+ 2026-08-04 the opposite had happened. A test now enumerates them and fixes
344
+ them — and declares what it does *not* cover: `/vl-nodes/invite` is federated
345
+ by design, and if that ever changes it should change with someone noticing.
346
+
7
347
  ## 0.8.51 — 2026-08-04 — "Who May Let Someone In"
8
348
 
9
349
  - **Minting a pairing invite no longer crosses the federation.** An invite is