@mmmbuto/nexuscrew 0.9.8 → 0.9.10
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 +141 -0
- package/frontend/dist/assets/index-CIJoChF1.css +32 -0
- package/frontend/dist/assets/index-DBGNS4gP.js +93 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/audio/groups.js +17 -3
- package/lib/cli/pidfile.js +2 -2
- package/lib/files/routes.js +35 -1
- package/lib/fleet/cell-lease-server.js +1 -1
- package/lib/fleet/managed.js +1 -1
- package/lib/fleet/routes.js +5 -0
- package/lib/live-host/bridge.js +31 -5
- package/lib/live-host/routes.js +1 -1
- package/lib/notify/routes.js +8 -1
- package/lib/server.js +5 -1
- package/lib/settings/routes.js +11 -2
- package/package.json +1 -1
- package/frontend/dist/assets/index-C2A08iwn.js +0 -93
- package/frontend/dist/assets/index-CM1TECdp.css +0 -32
package/frontend/dist/index.html
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
<meta name="apple-mobile-web-app-title" content="NexusCrew" />
|
|
12
12
|
<link rel="manifest" href="/manifest.json" />
|
|
13
13
|
<title>NexusCrew</title>
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-DBGNS4gP.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CIJoChF1.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="root"></div>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.9.
|
|
1
|
+
{"version":"0.9.10"}
|
package/lib/audio/groups.js
CHANGED
|
@@ -28,6 +28,20 @@ function emptyGroups() { return { schemaVersion: SCHEMA_VERSION, groups: {} }; }
|
|
|
28
28
|
|
|
29
29
|
function validName(name) { return typeof name === 'string' && GROUP_NAME_RE.test(name); }
|
|
30
30
|
|
|
31
|
+
// R31-A4: gli errori di VALIDAZIONE si distinguono per contratto (status 400 +
|
|
32
|
+
// code chiuso), non per testo del messaggio. Prima erano Error generici
|
|
33
|
+
// indistinguibili da quelli di I/O di atomicWrite (EACCES/ENOSPC/...), e la
|
|
34
|
+
// route poteva separarli solo indovinando con un match sulla stringa — il
|
|
35
|
+
// modo sbagliato, vietato qui. Gli errori fs restano nativi: portano il loro
|
|
36
|
+
// errno in .code e NON hanno .status, così una scrittura fallita non può mai
|
|
37
|
+
// essere scambiata per una validazione.
|
|
38
|
+
function validationError(message, code) {
|
|
39
|
+
const err = new Error(message);
|
|
40
|
+
err.status = 400;
|
|
41
|
+
err.code = code;
|
|
42
|
+
return err;
|
|
43
|
+
}
|
|
44
|
+
|
|
31
45
|
function normalizeSpec(spec) {
|
|
32
46
|
if (!spec || typeof spec !== 'object' || Array.isArray(spec)) return null;
|
|
33
47
|
if (Object.keys(spec).some((key) => !['targets', 'mode'].includes(key))) return null;
|
|
@@ -96,9 +110,9 @@ function getGroup(cfg = {}, name, home) {
|
|
|
96
110
|
}
|
|
97
111
|
|
|
98
112
|
function saveGroup(cfg = {}, name, spec, home) {
|
|
99
|
-
if (!validName(name)) throw
|
|
113
|
+
if (!validName(name)) throw validationError('nome gruppo audio non valido', 'AUDIO_GROUP_NAME_INVALID');
|
|
100
114
|
const normalized = normalizeSpec(spec);
|
|
101
|
-
if (!normalized) throw
|
|
115
|
+
if (!normalized) throw validationError('gruppo audio non valido', 'AUDIO_GROUP_SPEC_INVALID');
|
|
102
116
|
const store = readGroups(cfg, home);
|
|
103
117
|
store.groups[name] = normalized;
|
|
104
118
|
atomicWrite(groupsPath(cfg, home), store);
|
|
@@ -106,7 +120,7 @@ function saveGroup(cfg = {}, name, spec, home) {
|
|
|
106
120
|
}
|
|
107
121
|
|
|
108
122
|
function removeGroup(cfg = {}, name, home) {
|
|
109
|
-
if (!validName(name)) throw
|
|
123
|
+
if (!validName(name)) throw validationError('nome gruppo audio non valido', 'AUDIO_GROUP_NAME_INVALID');
|
|
110
124
|
const store = readGroups(cfg, home);
|
|
111
125
|
if (!Object.prototype.hasOwnProperty.call(store.groups, name)) return false;
|
|
112
126
|
delete store.groups[name];
|
package/lib/cli/pidfile.js
CHANGED
|
@@ -72,7 +72,7 @@ function readProcessStart(pid) {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
// Campi che SOLO writePidfile puo' scrivere: mai dal chiamante via `extra`.
|
|
75
|
-
// Misurato (
|
|
75
|
+
// Misurato (2026-08-17): con lo spread di extra per ultimo,
|
|
76
76
|
// writePidfile(f, pid, cmd, {processStart:'FINTO', attestation:'unsupported'})
|
|
77
77
|
// vinceva sul valore vero calcolato da probeProcessStart — un chiamante
|
|
78
78
|
// poteva scrivere un'attestazione INVENTATA. safeExtra la filtra qui, e i
|
|
@@ -534,7 +534,7 @@ function killPidfile(p, signal = 'SIGTERM', impl = {}) {
|
|
|
534
534
|
// scrivere qui, verificato nell'istante in cui
|
|
535
535
|
// conta, non dedotto da uno passato. Si rifiuta.
|
|
536
536
|
//
|
|
537
|
-
// MISURATO (
|
|
537
|
+
// MISURATO (2026-08-17): trattare OGNI meta senza attestazione come
|
|
538
538
|
// indeterminate rompe l'aggiornamento automatico per ogni nodo il cui
|
|
539
539
|
// runtime e' ancora precedente a quando processStart e' nato (8fe514f,
|
|
540
540
|
// v0.9.0) — npm install sovrascrive pidfile.js PRIMA che il runner lo
|
package/lib/files/routes.js
CHANGED
|
@@ -6,6 +6,7 @@ const express = require('express');
|
|
|
6
6
|
const { Router } = require('express');
|
|
7
7
|
const multer = require('multer');
|
|
8
8
|
const store = require('./store.js');
|
|
9
|
+
const { pasteArgs } = require('../tmux/actions.js');
|
|
9
10
|
|
|
10
11
|
// Router file-exchange. Nessuno stato: tutto deriva da cfg + filesystem.
|
|
11
12
|
// notifier (opzionale, MCP bridge): emette la notify di consegna file outbox.
|
|
@@ -32,7 +33,40 @@ function filesRoutes({ cfg, sessionExists, paste, notifier, readonly = () => fal
|
|
|
32
33
|
// paste=false (tasto allegati del composer): il client appende il path al
|
|
33
34
|
// testo del composer — niente scrittura PTY. Default = incolla (FilesPanel).
|
|
34
35
|
const wantPaste = String((req.body && req.body.paste) || '') !== 'false';
|
|
35
|
-
|
|
36
|
+
// R31-A2: paste RICHIESTO ma non riuscito non e' un 200 con pasted:false —
|
|
37
|
+
// tre cause collassavano in quell'unica etichetta muta (non richiesto /
|
|
38
|
+
// tmux non ha ricevuto / testo respinto) e nessuno, risposta o UI, poteva
|
|
39
|
+
// accorgersene. Il vocabolario e' quello del ramo answer degli ask
|
|
40
|
+
// (lib/notify/routes.js): «paste fallito» si dice non-200 + error, col
|
|
41
|
+
// codice che nomina il soggetto giusto (502 valle raggiunto, 500 rifiuto
|
|
42
|
+
// nostro). Il file resta nella risposta: e' salvato in inbox, e' la
|
|
43
|
+
// consegna alla PTY che non e' avvenuta — la causa vera va detta, non
|
|
44
|
+
// nascosta dietro un OK.
|
|
45
|
+
if (!wantPaste) return res.json({ ...saved, pasted: false });
|
|
46
|
+
// Testo respinto a monte: pasteArgs e' la FONTE UNICA del rifiuto (la usa
|
|
47
|
+
// pasteToSession). Il nome upload e' sanitizzato: se scatta, la causa e'
|
|
48
|
+
// il filesRoot di configurazione (control char) o un path oltre i 4096
|
|
49
|
+
// char. Rifiuto noto PRIMA di tmux: la PTY non va neanche tentata.
|
|
50
|
+
// 500, non 502 (rilievo audit su d6c47c6): qui il valle NON e' stato
|
|
51
|
+
// contattato — a rifiutare siamo noi, per configurazione nostra. 502
|
|
52
|
+
// affermerebbe «il servizio a monte non risponde» e sveglierebbe un
|
|
53
|
+
// allarme su un tmux che sta benissimo. La coppia di codici e'
|
|
54
|
+
// informazione: 502 = raggiunto ma non ha preso; 500 = mai tentato.
|
|
55
|
+
if (!pasteArgs(session, saved.path)) {
|
|
56
|
+
return res.status(500).json({
|
|
57
|
+
error: 'paste fallito: path non incollabile (control char o oltre 4096 char)',
|
|
58
|
+
...saved,
|
|
59
|
+
pasted: false,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const pasted = await paste(session, saved.path);
|
|
63
|
+
if (!pasted) {
|
|
64
|
+
return res.status(502).json({
|
|
65
|
+
error: `paste fallito: sessione "${session}" non raggiungibile`,
|
|
66
|
+
...saved,
|
|
67
|
+
pasted: false,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
36
70
|
res.json({ ...saved, pasted });
|
|
37
71
|
});
|
|
38
72
|
});
|
|
@@ -264,7 +264,7 @@ function createLeaseManager(cfg = {}, seams = {}) {
|
|
|
264
264
|
socket.on('data', onData);
|
|
265
265
|
socket.once('close', onEOF);
|
|
266
266
|
socket.once('end', onEOF);
|
|
267
|
-
// Correzione (audit 2a, segnalazione precisata
|
|
267
|
+
// Correzione (audit 2a, segnalazione precisata in revisione): questo E' il socket
|
|
268
268
|
// su cui il server SCRIVE (l'ack di refresh — proprio dove l'EPIPE nasce se
|
|
269
269
|
// il peer muore mentre la write e' in volo). Un EventEmitter che emette
|
|
270
270
|
// 'error' senza listener fa un throw che termina l'INTERO processo — non
|
package/lib/fleet/managed.js
CHANGED
|
@@ -325,7 +325,7 @@ const CATALOG = Object.freeze([
|
|
|
325
325
|
// default 'standard'; unsafe -> --always-approve (flag reale di `grok`).
|
|
326
326
|
{ id: 'grok.native', client: 'grok', provider: 'native', label: 'Grok account (CLI login)', auth: 'login', protocol: 'grok_native', core: true },
|
|
327
327
|
|
|
328
|
-
// VL/Vivling (
|
|
328
|
+
// VL/Vivling (repository `vl`): runtime TUI locale. Auth propria del
|
|
329
329
|
// runtime (default local Ollama, o VL_API_KEY/--profile in config.toml):
|
|
330
330
|
// NexusCrew non legge ne' copia credenziali (auth 'none'). Il CLI non ha flag
|
|
331
331
|
// prompt, --model ne' di approvazione (`vl [OPTIONS]`): lancia la TUI senza
|
package/lib/fleet/routes.js
CHANGED
|
@@ -93,6 +93,11 @@ function fleetRoutes(fleetP, cfg = {}) {
|
|
|
93
93
|
bootOwner: 'none', // §9b: provider non disponibile -> nessun boot owner
|
|
94
94
|
capabilities: capList(fleet),
|
|
95
95
|
...(fleet.reason ? { reason: fleet.reason } : {}),
|
|
96
|
+
// R33: il codice macchina del boot bloccato viaggia fino al client.
|
|
97
|
+
// La policy di lettura decide su QUESTO, non sulla prosa del reason
|
|
98
|
+
// (la prosa resta ripiego per i reason che un codice non ce l'hanno).
|
|
99
|
+
// Senza campo dal provider, la risposta semplicemente non lo ha.
|
|
100
|
+
...(fleet.migrationCode ? { migrationCode: fleet.migrationCode } : {}),
|
|
96
101
|
});
|
|
97
102
|
}
|
|
98
103
|
const st = await fleet.status();
|
package/lib/live-host/bridge.js
CHANGED
|
@@ -71,7 +71,7 @@ const CLIENT_NAME = 'nexuscrew-live-bridge';
|
|
|
71
71
|
// ponte, che e' gia' stata data.
|
|
72
72
|
const ORPHAN_GRACE_MS = 1500;
|
|
73
73
|
|
|
74
|
-
// —— Prompt per-cella (rev4 LC2, nome fisso confermato
|
|
74
|
+
// —— Prompt per-cella (rev4 LC2, nome fisso confermato il 2026-08-15) ——
|
|
75
75
|
// Collocazione: filesRoot/<tmuxSession>/LIVE_PROMPT.md — la sessione tmux
|
|
76
76
|
// ESATTA che il roster dichiara per la cella designata, la stessa fonte gia'
|
|
77
77
|
// usata per l'intestazione R2 (identityHeader). NON un prefisso ricostruito a
|
|
@@ -126,13 +126,39 @@ function readCellPrompt(filesRoot, tmuxSession) {
|
|
|
126
126
|
// di identità — è esattamente il difetto visto sul campo (la voce andava a
|
|
127
127
|
// leggere tmux per capire dove si trovava).
|
|
128
128
|
//
|
|
129
|
+
// R30 (v2, 2026-08-19): l'intestazione dice anche COME raggiungere i tool
|
|
130
|
+
// NexusCrew. Il daemon app-server espone UN solo insieme di server MCP a
|
|
131
|
+
// tutte le Live, con l'ambiente del daemon: nexuscrew è l'unico che prende
|
|
132
|
+
// l'identità dall'ambiente ereditato, quindi i suoi tool con sessione
|
|
133
|
+
// resterebbero fail-closed (nc_identity: MISSING). La via d'uscita ce l'ha
|
|
134
|
+
// il ponte: il nome esatto della sessione, che il server MCP accetta da
|
|
135
|
+
// NEXUSCREW_MCP_SESSION via stdio. Il valore NON si mette in systemd
|
|
136
|
+
// Environment= (una identità statica condivisa = impersonare una cella
|
|
137
|
+
// fissa, demolito in audit v1): lo dice l'intestazione, per-cella.
|
|
138
|
+
//
|
|
129
139
|
// Il fatto, niente di più: quale cella (id Fleet) e, se il roster la dichiara,
|
|
130
140
|
// la sessione tmux esatta — quella con cui la voce raggiunge i canonici della
|
|
131
|
-
// cella in ~/NexusFiles/<tmuxSession
|
|
132
|
-
//
|
|
141
|
+
// cella in ~/NexusFiles/<tmuxSession>/ — e la via ai tool per quella sessione.
|
|
142
|
+
// Restano qui FUORI le istruzioni di lavoro: quelle vivono nel prompt
|
|
143
|
+
// per-cella, che questa intestazione PRECEDE sempre.
|
|
144
|
+
//
|
|
145
|
+
// Senza tmuxSession dichiarata non c'è identità possibile: il testo lo DICE,
|
|
146
|
+
// non suggerisce un comando che fallirebbe comunque (e una sessione indovinata
|
|
147
|
+
// sarebbe l'identità di un'altra cella).
|
|
133
148
|
function identityHeader(cellId, tmuxSession) {
|
|
134
|
-
|
|
135
|
-
|
|
149
|
+
if (!tmuxSession) {
|
|
150
|
+
return `Live NexusCrew agganciata alla cella ${cellId}. `
|
|
151
|
+
+ 'Il roster non dichiara una sessione tmux per questa cella: senza sessione '
|
|
152
|
+
+ 'non c\'è identità, quindi i tool NexusCrew che la richiedono non sono '
|
|
153
|
+
+ 'raggiungibili da questa Live.';
|
|
154
|
+
}
|
|
155
|
+
return `Live NexusCrew agganciata alla cella ${cellId} (sessione tmux ${tmuxSession}). `
|
|
156
|
+
+ 'Questa Live eredita l\'ambiente del daemon, condiviso fra tutte le Live e senza identità: '
|
|
157
|
+
+ 'i tool NexusCrew che richiedono la sessione restano chiusi finché non li chiami con la tua. '
|
|
158
|
+
+ 'Per usarli avvia il server MCP NexusCrew via stdio con la sessione di questa cella '
|
|
159
|
+
+ `nell'ambiente — NEXUSCREW_MCP_SESSION=${tmuxSession} nexuscrew mcp — e parlagli `
|
|
160
|
+
+ 'JSON-RPC su stdin (initialize, notifications/initialized, tools/call). '
|
|
161
|
+
+ `Il valore esatto per questa conversazione è ${tmuxSession}: mai un'altra sessione.`;
|
|
136
162
|
}
|
|
137
163
|
|
|
138
164
|
// —— Client on-demand del socket di controllo (sezione protocollo sopra) ——
|
package/lib/live-host/routes.js
CHANGED
|
@@ -39,7 +39,7 @@ function isActive(cell) {
|
|
|
39
39
|
return !!(cell && cell.active === true && cell.tmux !== false);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
// --- Seam lease↔designazione (2026-08-15, decisione
|
|
42
|
+
// --- Seam lease↔designazione (2026-08-15, decisione presa in revisione: grace = false) -----
|
|
43
43
|
//
|
|
44
44
|
// L'idoneita' dell'host designato non e' piu' solo «sessione tmux viva»: con
|
|
45
45
|
// remain-on-exit la sessione sopravvive alla morte del supervisore, e la
|
package/lib/notify/routes.js
CHANGED
|
@@ -186,7 +186,14 @@ function notifyRoutes({
|
|
|
186
186
|
originNode: resolved.origin.node,
|
|
187
187
|
originCell: resolved.origin.cell,
|
|
188
188
|
});
|
|
189
|
-
|
|
189
|
+
// R31-A3: lo status e' DERIVATO dai conteggi, non dichiarato a parte.
|
|
190
|
+
// `emit` e' best-effort — push fallito → 0, `ui` conta i write SSE
|
|
191
|
+
// riusciti — e il dispatcher propaga SOLO l'etichetta (i conteggi
|
|
192
|
+
// muoiono in forward(), rilievo R1/rc.14): per la cella mittente e'
|
|
193
|
+
// tutta l'informazione. Non puo' affermare una consegna che i conteggi
|
|
194
|
+
// smentiscono: zero canali raggiunti → 'no-delivery'.
|
|
195
|
+
const status = delivered.ui + delivered.push > 0 ? 'delivered' : 'no-delivery';
|
|
196
|
+
return res.json({ status, delivered });
|
|
190
197
|
}
|
|
191
198
|
|
|
192
199
|
// --- target remoto: instrada, non consegnare qui -----------------------
|
package/lib/server.js
CHANGED
|
@@ -864,7 +864,11 @@ function createServer(opts = {}) {
|
|
|
864
864
|
peers: federatedPeers,
|
|
865
865
|
localPort: () => (server && server.address() ? server.address().port : cfg.port),
|
|
866
866
|
localToken: () => tokenHolder.value,
|
|
867
|
-
|
|
867
|
+
// 'no-delivery' (R31-A3): esito legittimo del target — richiesta accettata
|
|
868
|
+
// ma nessun canale raggiunto (0 UI, 0 push). Senza questa voce forward()
|
|
869
|
+
// lo degraderebbe a 'unknown/unreadable-endpoint-result', nascondendo
|
|
870
|
+
// proprio il silenzio che l'esito esiste per rivelare.
|
|
871
|
+
statuses: new Set(['delivered', 'no-delivery', 'refused', 'unreachable', 'unknown']),
|
|
868
872
|
}),
|
|
869
873
|
federatedRate: createSpeakRateLimiter(),
|
|
870
874
|
}));
|
package/lib/settings/routes.js
CHANGED
|
@@ -928,13 +928,22 @@ function settingsRoutes(deps = {}) {
|
|
|
928
928
|
return send(res, 400, { error: 'body non valido: attesi targets e mode' });
|
|
929
929
|
}
|
|
930
930
|
try { return send(res, 200, { group: audioGroups.saveGroup(audioCfg, String(req.params.name || ''), body, home) }); }
|
|
931
|
-
|
|
931
|
+
// R31-A4: il catch nudo raccontava QUALUNQUE errore come «gruppo non
|
|
932
|
+
// valido» (400): un EACCES/ENOSPC sulla scrittura di audio-groups.json
|
|
933
|
+
// finiva letto come colpa del nome. La validazione arriva dal modulo con
|
|
934
|
+
// status 400 + code chiuso; tutto il resto — scrittura fallita compresa —
|
|
935
|
+
// è 500 con la causa vera (scrubError ripulisce i path). Stessa formula
|
|
936
|
+
// delle route settings sopra.
|
|
937
|
+
catch (e) { return send(res, e.status || 500, { error: scrubError(e), ...(e.code ? { code: e.code } : {}) }); }
|
|
932
938
|
});
|
|
933
939
|
r.delete('/audio/groups/:name', mutGate, (req, res) => {
|
|
934
940
|
try {
|
|
935
941
|
const removed = audioGroups.removeGroup(audioCfg, String(req.params.name || ''), home);
|
|
936
942
|
return removed ? send(res, 200, { removed: true }) : send(res, 404, { error: 'gruppo audio non trovato' });
|
|
937
|
-
} catch (
|
|
943
|
+
} catch (e) {
|
|
944
|
+
// R31-A4: come PUT sopra — vedi il commento lì.
|
|
945
|
+
return send(res, e.status || 500, { error: scrubError(e), ...(e.code ? { code: e.code } : {}) });
|
|
946
|
+
}
|
|
938
947
|
});
|
|
939
948
|
|
|
940
949
|
// Viewer-local aliases for routed nodes. These routes are local-only and do
|