@mmmbuto/nexuscrew 0.9.16 → 0.9.18
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 +157 -3
- package/docs/ALIBABA_TOKEN_PLAN.md +70 -0
- package/docs/AUDIO_SHARE.md +119 -0
- package/docs/CELL_PANEL.md +156 -0
- package/docs/CONFIGURATION.md +122 -0
- package/docs/FLEET.md +222 -0
- package/docs/INSTALLATION.md +128 -0
- package/docs/LIVE_PROMPT.md +110 -0
- package/docs/MCP.md +151 -0
- package/docs/NODES.md +214 -0
- package/docs/NOTIFICATIONS.md +101 -0
- package/docs/OPERATIONS.md +114 -0
- package/docs/README.md +55 -0
- package/docs/SECURITY.md +185 -0
- package/docs/VL_MICRO_NODES.md +107 -0
- package/docs/img/fleet-deck-desktop.png +0 -0
- package/docs/img/session-mobile.png +0 -0
- package/docs/live-prompt-templates/LIVE_PROMPT.en.md +67 -0
- package/docs/live-prompt-templates/LIVE_PROMPT.es.md +70 -0
- package/docs/live-prompt-templates/LIVE_PROMPT.it.md +67 -0
- package/frontend/dist/assets/{index-CwsGpQwK.js → index-DmpQoCsc.js} +1 -1
- package/frontend/dist/index.html +1 -1
- package/frontend/dist/version.json +1 -1
- package/lib/auth/middleware.js +1 -1
- package/lib/cells/scope-guard.js +2 -2
- package/lib/cli/commands.js +7 -7
- package/lib/cli/fleet-service.js +2 -2
- package/lib/cli/pidfile.js +15 -15
- package/lib/cli/service.js +2 -2
- package/lib/cli/stable-alias.js +1 -1
- package/lib/files/routes.js +2 -2
- package/lib/fleet/builtin.js +6 -6
- package/lib/fleet/catalogs/zai-responses.json +82 -0
- package/lib/fleet/cell-exec.js +4 -4
- package/lib/fleet/cell-lease-server.js +17 -17
- package/lib/fleet/definitions.js +28 -17
- package/lib/fleet/launch-broker.js +3 -3
- package/lib/fleet/lease-client.js +1 -1
- package/lib/fleet/managed.js +86 -26
- package/lib/fleet/prompt-delivery.js +1 -1
- package/lib/fleet/runtime.js +3 -3
- package/lib/live-host/bridge.js +3 -3
- package/lib/live-host/routes.js +1 -1
- package/lib/mcp/server.js +7 -8
- package/lib/mcp/tools.js +1 -1
- package/lib/nodes/commands.js +2 -2
- package/lib/nodes/health.js +1 -1
- package/lib/nodes/identity.js +10 -13
- package/lib/nodes/store.js +2 -2
- package/lib/nodes/tunnel-supervisor.js +7 -7
- package/lib/nodes/tunnel.js +10 -11
- package/lib/notify/asks.js +4 -4
- package/lib/notify/persist.js +1 -1
- package/lib/notify/push.js +4 -4
- package/lib/notify/routes.js +12 -12
- package/lib/proxy/node-proxy.js +4 -4
- package/lib/proxy/panel-proxy.js +2 -2
- package/lib/pty/attach.js +1 -1
- package/lib/server.js +3 -3
- package/lib/settings/pairing-coordinator.js +1 -1
- package/lib/settings/routes.js +4 -4
- package/lib/tmux/lifecycle.js +4 -4
- package/lib/tmux/preview.js +1 -1
- package/lib/update/runner.js +9 -9
- package/lib/vl-nodes/broker.js +1 -1
- package/lib/ws/bridge.js +1 -1
- package/package.json +20 -2
- package/frontend/index.html +0 -20
package/lib/nodes/identity.js
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
// secondo scrittore — la proprieta' regge per assenza, non per controllo, e la
|
|
29
29
|
// differenza va detta invece che lasciata credere.
|
|
30
30
|
//
|
|
31
|
-
// Rilievo
|
|
31
|
+
// Rilievo della revisione indipendente: una funzione provata e senza chiamanti e' il
|
|
32
32
|
// segnale piu' forte che una garanzia esista, ed e' il piu' facile da dare per
|
|
33
33
|
// sbaglio.
|
|
34
34
|
const fs = require('node:fs');
|
|
@@ -79,14 +79,11 @@ function readKeyFile(p) {
|
|
|
79
79
|
+ 'la chiave privata e\' leggibile oltre il proprietario, va rigenerata a mano');
|
|
80
80
|
}
|
|
81
81
|
// UN FILE CHE ESISTE MA NON E' USABILE FA RUMORE, NON SI SOSTITUISCE.
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
// con una chiave che non corrisponde. Meglio fermarsi e farlo sapere.
|
|
88
|
-
// Rilievo dell'audit indipendente; risolto nel verso opposto a quello
|
|
89
|
-
// proposto, perche' la coerenza va cercata sul ramo sicuro.
|
|
82
|
+
// Uno schema errato e un JSON corrotto sono due forme dello stesso stato non
|
|
83
|
+
// usabile: entrambi devono fare rumore e nessuno deve autorizzare una nuova
|
|
84
|
+
// chiave. Rigenerare cambia l'identita' del nodo e lascia ogni peer gia'
|
|
85
|
+
// associato con una chiave che non corrisponde. Il ramo sicuro si ferma e
|
|
86
|
+
// rende esplicita la causa senza sostituire il file.
|
|
90
87
|
let parsed;
|
|
91
88
|
try {
|
|
92
89
|
parsed = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
@@ -136,10 +133,10 @@ function writeKeyFile(p, privateKeyPem) {
|
|
|
136
133
|
// ensureNodeKey: idempotente. La prima chiamata genera, le successive leggono.
|
|
137
134
|
// `created` distingue i due casi per chi vuole registrarlo una volta sola.
|
|
138
135
|
// `afterWriteSeam` e' un seam di prova, come `sessionExistsSeam` e `fleetSeam`
|
|
139
|
-
// altrove: viene chiamato subito dopo la scrittura e
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
136
|
+
// altrove: viene chiamato subito dopo la scrittura e rende deterministico il
|
|
137
|
+
// punto della corsa. Senza il seam, il test puo' osservare un file gia'
|
|
138
|
+
// esistente e non esercitare mai il percorso di CREAZIONE; il controllo
|
|
139
|
+
// negativo deve quindi fissare esplicitamente quel punto.
|
|
143
140
|
function ensureNodeKey({ home, keyPath, afterWriteSeam } = {}) {
|
|
144
141
|
const p = keyPath || keyPathFor(home);
|
|
145
142
|
const existing = readKeyFile(p);
|
package/lib/nodes/store.js
CHANGED
|
@@ -80,7 +80,7 @@ function isAbsPath(p) {
|
|
|
80
80
|
// token remoto: segreto opaco, single-line, cap. Vuoto -> assente (non salvato).
|
|
81
81
|
// Charset ristretto a header-safe (VCHAR + spazio/tab): il token viene iniettato
|
|
82
82
|
// in `Authorization: Bearer <t>` verso l'upstream; un char fuori range farebbe
|
|
83
|
-
// lanciare setHeader in modo sincrono (ERR_INVALID_CHAR). (hardening
|
|
83
|
+
// lanciare setHeader in modo sincrono (ERR_INVALID_CHAR). (hardening).
|
|
84
84
|
function validToken(t) {
|
|
85
85
|
return typeof t === 'string' && t.length > 0 && t.length <= MAX_TOKEN_LEN
|
|
86
86
|
&& /^[\x20-\x7e\t]+$/.test(t);
|
|
@@ -589,7 +589,7 @@ function initStore(p) {
|
|
|
589
589
|
}
|
|
590
590
|
|
|
591
591
|
// Compatibilita' API interna: il vecchio nome non inizializza piu' a runtime.
|
|
592
|
-
// Tenerlo strict impedisce che un call-site dimenticato reintroduca
|
|
592
|
+
// Tenerlo strict impedisce che un call-site dimenticato reintroduca quel difetto.
|
|
593
593
|
function loadOrInitStore(p) { return loadStoreStrict(p); }
|
|
594
594
|
|
|
595
595
|
// --- Mutazioni pure (ritornano un nuovo store; il caller lo scrive) ---------
|
|
@@ -21,7 +21,7 @@ const ownershipGraceMs = Number.isFinite(ownershipGraceRaw) && ownershipGraceRaw
|
|
|
21
21
|
const reverseFailureMaxRaw = Number(process.env.NEXUSCREW_TUNNEL_REVERSE_FAILURE_MAX || 8);
|
|
22
22
|
const reverseFailureMax = Number.isInteger(reverseFailureMaxRaw) && reverseFailureMaxRaw >= 1
|
|
23
23
|
? Math.min(reverseFailureMaxRaw, 32) : 8;
|
|
24
|
-
//
|
|
24
|
+
// da revisione — contratto per la sonda del canale -L quando non conclude MAI:
|
|
25
25
|
// continuare a sondare ogni 250ms e' giusto per la finestra transitoria (il
|
|
26
26
|
// servizio remoto non e' ancora su dopo un restart/aggiornamento: la prossima
|
|
27
27
|
// sonda e' una connessione FRESCA, si qualifica da sola appena il servizio
|
|
@@ -35,7 +35,7 @@ const reverseFailureMax = Number.isInteger(reverseFailureMaxRaw) && reverseFailu
|
|
|
35
35
|
// quella soglia il canale entra in degraded, ESATTAMENTE come il fallimento
|
|
36
36
|
// del forward inverso — stesso stato, stessa auto-guarigione a cadenza fissa,
|
|
37
37
|
// mai un "pronto" dichiarato senza averlo verificato (sarebbe la stessa bugia
|
|
38
|
-
// che
|
|
38
|
+
// che una revisione ha tolto, solo con una bandiera "non verificato" appesa sopra).
|
|
39
39
|
const channelProbeMaxRaw = Number(process.env.NEXUSCREW_TUNNEL_CHANNEL_PROBE_MAX || 60);
|
|
40
40
|
const channelProbeMax = Number.isInteger(channelProbeMaxRaw) && channelProbeMaxRaw >= 1
|
|
41
41
|
? Math.min(channelProbeMaxRaw, 240) : 60;
|
|
@@ -112,7 +112,7 @@ function clearForwardProbe() {
|
|
|
112
112
|
// forces OpenSSH to establish the real forward channel. Only that event may
|
|
113
113
|
// advertise transport-ready or reset retry backoff.
|
|
114
114
|
//
|
|
115
|
-
//
|
|
115
|
+
// da revisione: connect NON è prova di canale. OpenSSH accetta la TCP sul listener
|
|
116
116
|
// locale SUBITO e chiede il canale al server DOPO: se il server lo nega
|
|
117
117
|
// (permitopen senza quella destinazione) il socket viene CHIUSO nel giro di
|
|
118
118
|
// millisecondi — bind locale riuscito, canale morto. Una finestra di grazia
|
|
@@ -159,7 +159,7 @@ function probeForwardChannels({ ports, graceMs = CHANNEL_GRACE_MS, connect = net
|
|
|
159
159
|
});
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
//
|
|
162
|
+
// da revisione punto 3: chi ha installato la chiave quando il pannello non esisteva è
|
|
163
163
|
// rotto e NON lo sapeva. Il prodotto lo DICE, con la riga da sostituire: le
|
|
164
164
|
// destinazioni arrivano dagli `-L`, la pubblica si DERIVA dalla privata
|
|
165
165
|
// indicata da `-i`. Quando non si riesce a derivarla, dice comunque COSA
|
|
@@ -359,7 +359,7 @@ function enterDegraded(diagnosis) {
|
|
|
359
359
|
retryTimer = setTimeout(run, steadyRetryMs);
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
//
|
|
362
|
+
// secondo difetto della stessa famiglia (da revisione interna):
|
|
363
363
|
// enterDegraded per il canale -L riusava la macchina del forward inverso
|
|
364
364
|
// senza la sua precondizione implicita. Nel reverse failure `child` e' GIA'
|
|
365
365
|
// null quando enterDegraded gira (handleFailure lo azzera PRIMA, perche' e'
|
|
@@ -369,7 +369,7 @@ function enterDegraded(diagnosis) {
|
|
|
369
369
|
// generazione. run() faceva `child = spawn(...)` incondizionatamente: la
|
|
370
370
|
// vecchia generazione, ancora viva, restava senza piu' nessuna variabile che
|
|
371
371
|
// la referenzi — irraggiungibile da stop(), orfana, titolare dei suoi bind
|
|
372
|
-
// per sempre. Misurato
|
|
372
|
+
// per sempre. Misurato in revisione: due fake-ssh vivi dopo un degraded,
|
|
373
373
|
// SIGTERM al supervisor ne ferma solo l'ultimo.
|
|
374
374
|
//
|
|
375
375
|
// La correzione non e' locale (un kill dentro enterDegraded): e' che
|
|
@@ -525,6 +525,6 @@ function acquireGeneration() {
|
|
|
525
525
|
}
|
|
526
526
|
if (require.main === module) acquireGeneration();
|
|
527
527
|
|
|
528
|
-
// Esportate per prova diretta (
|
|
528
|
+
// Esportate per prova diretta (da revisione): il main resta argv/env-driven e NON parte
|
|
529
529
|
// al require.
|
|
530
530
|
module.exports = { probeForwardChannels, refusalHint, refusalDetails, CHANNEL_GRACE_MS };
|
package/lib/nodes/tunnel.js
CHANGED
|
@@ -480,7 +480,7 @@ function supervisorExited(pid, timeoutMs = 2500, impl = {}) {
|
|
|
480
480
|
// Pre-flight sincrono: l'unico modo di "surfaccare" un binario ssh assente come
|
|
481
481
|
// failure ESPLICITA nel valore di ritorno (lo spawn emette 'error' asincrono, non
|
|
482
482
|
// catturabile sync). spawnSyncImpl iniettabile per test deterministici.
|
|
483
|
-
// ssh -V esiste su ogni platform supportata; ENOENT = binario mancante (
|
|
483
|
+
// ssh -V esiste su ogni platform supportata; ENOENT = binario mancante (da revisione).
|
|
484
484
|
function sshBinaryAvailable(sshBin, spawnSyncImpl) {
|
|
485
485
|
try {
|
|
486
486
|
const r = (spawnSyncImpl || spawnSync)(sshBin, ['-V'], { encoding: 'utf8' });
|
|
@@ -493,7 +493,7 @@ function sshBinaryAvailable(sshBin, spawnSyncImpl) {
|
|
|
493
493
|
// nel logfile del tunnel; pidfile con cmd verificabile (kill anti PID-reuse).
|
|
494
494
|
// spawnImpl/spawnSyncImpl iniettabili per i test (mai lancia ssh vero in suite).
|
|
495
495
|
//
|
|
496
|
-
// INVARIANTI (
|
|
496
|
+
// INVARIANTI (da revisione):
|
|
497
497
|
// - MAI crashare: spawn throw, error async, pid mancante -> failure esplicita
|
|
498
498
|
// {started:false, reason}, con cleanup di pidfile/process/log fd di nostra proprieta'.
|
|
499
499
|
// - NESSUN leak del log fd aperto internamente: il padre chiude la SUA copia dopo
|
|
@@ -558,7 +558,7 @@ function startTunnel(opts) {
|
|
|
558
558
|
};
|
|
559
559
|
|
|
560
560
|
// Pre-flight: binario ssh mancante -> failure esplicita, nessuno spawn, nessun
|
|
561
|
-
// crash (
|
|
561
|
+
// crash (da revisione: prima restituiva started:true e poi l'event 'error' killava Node).
|
|
562
562
|
if (!sshBinaryAvailable(sshBin, spawnSyncImpl)) {
|
|
563
563
|
closeOwnedFd();
|
|
564
564
|
return { started: false, reason: 'ssh binary not found', sshBin };
|
|
@@ -630,7 +630,7 @@ function startTunnel(opts) {
|
|
|
630
630
|
if (Number.isInteger(logFd)) {
|
|
631
631
|
try { fs.writeSync(logFd, `[nexuscrew] supervisor requested transport=${path.basename(sshBin)}\n`); } catch (_) {}
|
|
632
632
|
}
|
|
633
|
-
closeOwnedFd(); // copia del padre: il figlio ha la sua dup; nessun leak (
|
|
633
|
+
closeOwnedFd(); // copia del padre: il figlio ha la sua dup; nessun leak (da revisione)
|
|
634
634
|
return { started: true, pid, logPath, transport: sshBin };
|
|
635
635
|
}
|
|
636
636
|
|
|
@@ -694,7 +694,7 @@ function readSshVersion(spawnSyncImpl) {
|
|
|
694
694
|
} catch (_) { return null; }
|
|
695
695
|
}
|
|
696
696
|
|
|
697
|
-
//
|
|
697
|
+
// da revisione: la riga authorized_keys con le destinazioni EXPLICITE dei -L. Una sola
|
|
698
698
|
// porta quando il pannello non è (ancora) noto: nodesAdd non può conoscerla,
|
|
699
699
|
// la annuncia il peer nel join — e allora la riga la emette CHI ha
|
|
700
700
|
// l'informazione (pairing, supervisor). Due permitopen distinti, MAI un
|
|
@@ -715,11 +715,10 @@ function authorizedKeysLine({ remotePort, panelRemotePort, pub } = {}) {
|
|
|
715
715
|
|
|
716
716
|
// LA PUBBLICA SI DERIVA DALLA PRIVATA, non si legge dal file accanto.
|
|
717
717
|
//
|
|
718
|
-
//
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
//
|
|
722
|
-
// chiave valida — non che sia LA chiave di `A`. Se `A.pub` e' stale, ripristinato
|
|
718
|
+
// Validare il nome dell'algoritmo, la struttura del blob o il risultato di
|
|
719
|
+
// `ssh-keygen -l` su `A.pub` prova soltanto che il file contiene UNA chiave
|
|
720
|
+
// valida — non che sia LA chiave derivata dalla privata di `A`. Se `A.pub` e'
|
|
721
|
+
// stale, ripristinato
|
|
723
722
|
// da un backup o sostituito, si pubblica la chiave sbagliata: l'utente
|
|
724
723
|
// sostituisce la riga di A con quella di B, e al reconnect successivo A perde
|
|
725
724
|
// l'accesso. Il prodotto avrebbe causato il guasto che prometteva di riparare.
|
|
@@ -733,7 +732,7 @@ function authorizedKeysLine({ remotePort, panelRemotePort, pub } = {}) {
|
|
|
733
732
|
// batch non si puo' sbloccare) o `ssh-keygen` non c'e': senza poterla derivare
|
|
734
733
|
// non si compone nessuna riga.
|
|
735
734
|
// Esiti ENUMERATI della derivazione della pubblica (difetto strutturale
|
|
736
|
-
// registrato
|
|
735
|
+
// registrato in revisione: `null | stringa` comprimeva stati DIVERSI, e il
|
|
737
736
|
// livello basso componeva frasi su fatti che non conosce — meta' della 0.9.5
|
|
738
737
|
// e' nata li'). Gli esiti sono DATI, senza testo per l'utente: le frasi si
|
|
739
738
|
// compongono al confine UI/log, mai qui. Si aggancia a P4: una chiave cifrata
|
package/lib/notify/asks.js
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
// Stato in-memory + persistenza <dir>/asks.json 0600 (sopravvive al restart:
|
|
4
4
|
// un ask aperto resta risponibile). Lo store e' PURO stato: il paste tmux e la
|
|
5
5
|
// notify li orchestra la route (lib/notify/routes.js), che qui fa il ciclo
|
|
6
|
-
// claim -> (paste) -> commit/release (
|
|
6
|
+
// claim -> (paste) -> commit/release (da revisione: una sola answer puo' vincere).
|
|
7
7
|
const path = require('node:path');
|
|
8
8
|
const crypto = require('node:crypto');
|
|
9
9
|
const { readJsonSafe, atomicWriteJson } = require('./persist.js');
|
|
10
10
|
|
|
11
11
|
const ASKS_FILE = 'asks.json';
|
|
12
|
-
//
|
|
12
|
+
// da revisione: MAX_OPEN e' un cap DURO sugli ask aperti — al cap il nuovo ask viene
|
|
13
13
|
// RIFIUTATO (reason 'cap'), mai droppato uno aperto. MAX_KEEP pota solo gli
|
|
14
14
|
// answered piu' vecchi dal file.
|
|
15
15
|
const MAX_OPEN = 100;
|
|
@@ -72,7 +72,7 @@ function createAsksStore(opts = {}) {
|
|
|
72
72
|
function create({ question, options, session }) {
|
|
73
73
|
const v = validate({ question, options });
|
|
74
74
|
if (!v.ok) return { ok: false, reason: 'invalid', error: v.error };
|
|
75
|
-
//
|
|
75
|
+
// da revisione: cap duro sugli aperti — rifiuto esplicito, MAI drop di ask aperti.
|
|
76
76
|
if (openCount() >= MAX_OPEN) {
|
|
77
77
|
return {
|
|
78
78
|
ok: false,
|
|
@@ -103,7 +103,7 @@ function createAsksStore(opts = {}) {
|
|
|
103
103
|
.map((a) => ({ ...a, ...(a.options ? { options: a.options.slice() } : {}) }));
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
// --- ciclo answer
|
|
106
|
+
// --- ciclo answer (da revisione): claim atomico open -> answering ---------------
|
|
107
107
|
// Node e' single-threaded ma il paste e' un await: due answer concorrenti
|
|
108
108
|
// superavano entrambe il check `answered` prima che una marcasse. Il claim
|
|
109
109
|
// sincrono (nessun await tra check e set) fa vincere UNA sola richiesta; le
|
package/lib/notify/persist.js
CHANGED
|
@@ -6,7 +6,7 @@ const fs = require('node:fs');
|
|
|
6
6
|
const path = require('node:path');
|
|
7
7
|
const crypto = require('node:crypto');
|
|
8
8
|
|
|
9
|
-
// Legge un JSON (oggetto) da file segreto. Fail-closed (
|
|
9
|
+
// Legge un JSON (oggetto) da file segreto. Fail-closed (da revisione): open
|
|
10
10
|
// O_NOFOLLOW + fstat sullo STESSO fd (niente race lstat->open), e un file
|
|
11
11
|
// preesistente con symlink, tipo non-regolare, owner inatteso o permessi di
|
|
12
12
|
// gruppo/altri viene RIFIUTATO con errore chiaro — mai riparato in silenzio
|
package/lib/notify/push.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// Il sender e' iniettabile (webpushImpl) cosi' i test non toccano MAI la rete;
|
|
8
8
|
// la chiave PRIVATA non esce da questo modulo.
|
|
9
9
|
//
|
|
10
|
-
//
|
|
10
|
+
// da revisione (threat model SSRF): sendNotification fa una richiesta server-side.
|
|
11
11
|
// Ogni endpoint e' https-only, viene risolto sia al subscribe sia immediatamente
|
|
12
12
|
// prima del send, e la richiesta usa un https.Agent con lookup PINNATO agli IP
|
|
13
13
|
// verificati. Cosi' un secondo lookup/DNS rebinding non puo' cambiare destinazione.
|
|
@@ -168,7 +168,7 @@ function createPushService(opts = {}) {
|
|
|
168
168
|
if (!opts.dir) throw new Error('createPushService: dir richiesta');
|
|
169
169
|
const vapidPath = path.join(opts.dir, VAPID_FILE);
|
|
170
170
|
const subsPath = path.join(opts.dir, SUBS_FILE);
|
|
171
|
-
//
|
|
171
|
+
// da revisione: closure iniettata dal server — in READONLY questo modulo non scrive MAI
|
|
172
172
|
// (niente generazione VAPID, niente cleanup subscription).
|
|
173
173
|
const readonly = typeof opts.readonly === 'function' ? opts.readonly : () => false;
|
|
174
174
|
const lookupImpl = opts.lookupImpl || dns.promises.lookup;
|
|
@@ -191,7 +191,7 @@ function createPushService(opts = {}) {
|
|
|
191
191
|
vapid = { publicKey: cur.publicKey, privateKey: cur.privateKey };
|
|
192
192
|
return vapid;
|
|
193
193
|
}
|
|
194
|
-
//
|
|
194
|
+
// da revisione: la generazione e' una SCRITTURA — vietata in READONLY (503 esplicito,
|
|
195
195
|
// niente vapid.json fantasma).
|
|
196
196
|
if (readonly()) {
|
|
197
197
|
const e = new Error('READONLY: chiavi VAPID assenti e non generabili (riavvia senza READONLY per il primo setup push)');
|
|
@@ -241,7 +241,7 @@ function createPushService(opts = {}) {
|
|
|
241
241
|
function count() { return readSubs().length; }
|
|
242
242
|
|
|
243
243
|
// Invia il payload a tutte le subscription. 404/410 (endpoint morto) -> la
|
|
244
|
-
// subscription si rimuove, MA NON in READONLY (
|
|
244
|
+
// subscription si rimuove, MA NON in READONLY (da revisione: riscrivere push.json e' una
|
|
245
245
|
// scrittura persistente; la notify viene comunque consegnata alle vive).
|
|
246
246
|
// Nessuna subscription -> {sent:0} senza generare VAPID (resta lazy davvero).
|
|
247
247
|
async function sendToAll(payload) {
|
package/lib/notify/routes.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
// Route del MCP bridge (design §2): notify, web-push, asks. Montate dentro il
|
|
3
3
|
// router /api gia' dietro requireToken (server.js) — qui restano solo:
|
|
4
|
-
// - READONLY come FLOOR (
|
|
4
|
+
// - READONLY come FLOOR (da revisione): sono gated 403 TUTTI i mutanti — answer
|
|
5
5
|
// (scrittura PTY via paste), push subscribe/unsubscribe (push.json) e la
|
|
6
6
|
// CREAZIONE di ask (persiste asks.json e genererebbe domande non
|
|
7
7
|
// risponibili dallo stesso server). L'UNICA eccezione dichiarata e' la
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
// push senza persistenza; anche il cleanup delle subscription morte e'
|
|
10
10
|
// sospeso in READONLY, vedi lib/notify/push.js). Le GET restano lettura
|
|
11
11
|
// pura: /push/vapid in READONLY non genera chiavi (503 se assenti).
|
|
12
|
-
// - rate-limit (
|
|
12
|
+
// - rate-limit (da revisione): il campo `session` e' dichiarato dal chiamante e
|
|
13
13
|
// NON e' un confine di sicurezza — il limite che conta e' GLOBALE per
|
|
14
14
|
// principal/token (un Bearer = un'installazione); il bucket per-sessione
|
|
15
15
|
// resta come fairness tra celle oneste. Stessa coppia di limiti sulla
|
|
16
|
-
// creazione ask (
|
|
16
|
+
// creazione ask (da revisione).
|
|
17
17
|
// - validazione input strict fail-closed (schema chiuso per ogni body).
|
|
18
18
|
// Il paste della risposta riusa ESATTAMENTE pasteToSession (bracketed literal,
|
|
19
19
|
// niente Invio, control char rifiutati): qui si sanifica il testo PRIMA.
|
|
@@ -43,7 +43,7 @@ const MAX_BODY = 2000;
|
|
|
43
43
|
const MAX_ANSWER = 3900;
|
|
44
44
|
const MAX_REPLY_LABEL = 48;
|
|
45
45
|
|
|
46
|
-
// Sliding window in-memory per chiave. La mappa ha un cap duro (
|
|
46
|
+
// Sliding window in-memory per chiave. La mappa ha un cap duro (da revisione): entry
|
|
47
47
|
// scadute potate a ogni giro, poi evizione LRU deterministica (ordine di
|
|
48
48
|
// iterazione della Map = ordine di ultimo uso, re-insert ad ogni allow).
|
|
49
49
|
// NB: l'evizione azzera il conteggio del bucket evitto — per questo il cap
|
|
@@ -68,7 +68,7 @@ function createRateLimiter({ max = RATE_MAX, windowMs = RATE_WINDOW_MS, maxBucke
|
|
|
68
68
|
return { allow, size: () => hits.size };
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
// Coppia di limiti
|
|
71
|
+
// Coppia di limiti (da revisione): globale per token (confine di sicurezza) + per
|
|
72
72
|
// sessione (fairness). Una richiesta oltre-limite consuma comunque il budget
|
|
73
73
|
// globale: anche lo spam rifiutato e' attivita' del principal.
|
|
74
74
|
function createSenderLimiter(rateCfg = {}) {
|
|
@@ -189,7 +189,7 @@ function notifyRoutes({
|
|
|
189
189
|
// R31-A3: lo status e' DERIVATO dai conteggi, non dichiarato a parte.
|
|
190
190
|
// `emit` e' best-effort — push fallito → 0, `ui` conta i write SSE
|
|
191
191
|
// riusciti — e il dispatcher propaga SOLO l'etichetta (i conteggi
|
|
192
|
-
// muoiono in forward(), rilievo
|
|
192
|
+
// muoiono in forward(), rilievo da revisione): per la cella mittente e'
|
|
193
193
|
// tutta l'informazione. Non puo' affermare una consegna che i conteggi
|
|
194
194
|
// smentiscono: zero canali raggiunti → 'no-delivery'.
|
|
195
195
|
const status = delivered.ui + delivered.push > 0 ? 'delivered' : 'no-delivery';
|
|
@@ -222,7 +222,7 @@ function notifyRoutes({
|
|
|
222
222
|
|
|
223
223
|
// --- web-push --------------------------------------------------------------
|
|
224
224
|
// GET lettura pura; in READONLY push.vapidPublicKey() NON genera chiavi e
|
|
225
|
-
// segnala 503 (e.status) se assenti —
|
|
225
|
+
// segnala 503 (e.status) se assenti — da revisione.
|
|
226
226
|
r.get('/push/vapid', (_req, res) => {
|
|
227
227
|
try { res.json({ publicKey: push.vapidPublicKey() }); }
|
|
228
228
|
catch (e) { res.status(e.status || 500).json({ error: String(e.message || e) }); }
|
|
@@ -232,7 +232,7 @@ function notifyRoutes({
|
|
|
232
232
|
try {
|
|
233
233
|
const sub = req.body && req.body.subscription;
|
|
234
234
|
const out = await push.subscribe(sub);
|
|
235
|
-
//
|
|
235
|
+
// da revisione: cap sul numero di subscription -> 429 (quota), input invalido -> 400.
|
|
236
236
|
if (!out.ok) return res.status(out.reason === 'cap' ? 429 : 400).json({ error: out.error });
|
|
237
237
|
res.json({ subscribed: true, count: out.count });
|
|
238
238
|
} catch (e) { res.status(500).json({ error: String(e.message || e) }); }
|
|
@@ -247,8 +247,8 @@ function notifyRoutes({
|
|
|
247
247
|
});
|
|
248
248
|
|
|
249
249
|
// --- asks ------------------------------------------------------------------
|
|
250
|
-
//
|
|
251
|
-
// che lo stesso server vieterebbe di rispondere.
|
|
250
|
+
// da revisione: gated READONLY (mutGate) — crea stato durevole (asks.json) e domande
|
|
251
|
+
// che lo stesso server vieterebbe di rispondere. da revisione: rate-limit creazione
|
|
252
252
|
// (globale per token + per sessione) + cap duro dello store -> 429.
|
|
253
253
|
r.post('/asks', mutGate, json, async (req, res) => {
|
|
254
254
|
try {
|
|
@@ -294,7 +294,7 @@ function notifyRoutes({
|
|
|
294
294
|
});
|
|
295
295
|
|
|
296
296
|
// Dismiss (scarta domanda): NON cancella la riga, la marca `dismissed` (lo
|
|
297
|
-
// storico serve). Stesso mutGate degli altri mutanti (
|
|
297
|
+
// storico serve). Stesso mutGate degli altri mutanti (da revisione: scrittura durevole).
|
|
298
298
|
// Idempotente; 404 se id inesistente; 409 se answering (claim attivo: non si
|
|
299
299
|
// scarta una risposta in corso). Emette il frame per le UI aperte come fa
|
|
300
300
|
// POST /asks con emitRaw: la card sparisce senza aspettare il poll.
|
|
@@ -312,7 +312,7 @@ function notifyRoutes({
|
|
|
312
312
|
} catch (e) { res.status(500).json({ error: String(e.message || e) }); }
|
|
313
313
|
});
|
|
314
314
|
|
|
315
|
-
// Answer: READONLY floor (il paste e' una scrittura PTY).
|
|
315
|
+
// Answer: READONLY floor (il paste e' una scrittura PTY). da revisione: il
|
|
316
316
|
// ciclo e' claim atomico (open -> answering, sincrono, PRIMA dell'await del
|
|
317
317
|
// paste) -> paste -> commit su successo / release su fallimento. Una sola
|
|
318
318
|
// richiesta concorrente vince; le altre vedono 409. Paste fallito -> 502 e
|
package/lib/proxy/node-proxy.js
CHANGED
|
@@ -42,7 +42,7 @@ const MUTATING = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
|
42
42
|
// passera' dalla via allowlistata della federazione, mai da questo pass-through
|
|
43
43
|
// generico — che inoltra QUALSIASI path non elencato qui.
|
|
44
44
|
// `/api/route` e `/federation/route` sono qui per una ragione che e' costata un
|
|
45
|
-
//
|
|
45
|
+
// da revisione: l'origine di una richiesta veniva dedotta dal PATH. Su /api/route il
|
|
46
46
|
// gestore assume di parlare col proprietario e non applica gate per-peer; ma un
|
|
47
47
|
// peer poteva farci arrivare quella forma ATTRAVERSO questo stesso pass-through
|
|
48
48
|
// (/node/<A>/api/route/_/panel/<cella>/...), e a quel punto il nodo di
|
|
@@ -182,7 +182,7 @@ function proxyHttp(req, res, node, rest, search, httpRequest) {
|
|
|
182
182
|
// http.request puo' lanciare in modo SINCRONO (es. header value con char
|
|
183
183
|
// invalido dal token del nodo) -> senza try/catch diventa un throw non gestito
|
|
184
184
|
// e Express emette una pagina 500 con lo stack (path interni). Chiudiamo in
|
|
185
|
-
// 502 JSON come gli altri errori upstream, senza esporre nulla. (fix
|
|
185
|
+
// 502 JSON come gli altri errori upstream, senza esporre nulla. (fix da revisione).
|
|
186
186
|
let upstream;
|
|
187
187
|
try {
|
|
188
188
|
upstream = httpRequest(options, (up) => {
|
|
@@ -298,9 +298,9 @@ function handleNodeUpgrade(ctx) {
|
|
|
298
298
|
if (settled) { try { upstream.destroy(); } catch (_) {} return; }
|
|
299
299
|
// buildUpgradeRequest/write possono lanciare (header invalido). Settle DOPO il
|
|
300
300
|
// successo dei write: cosi' il catch -> fail(502) e' OPERATIVO (distrugge entrambi
|
|
301
|
-
// i socket e invia 502) invece di no-op. Prima
|
|
301
|
+
// i socket e invia 502) invece di no-op. Prima della revisione settled veniva messo a
|
|
302
302
|
// true PRIMA dei write: un throw li' rendeva fail() un no-op e lasciava entrambi i
|
|
303
|
-
// socket vivi (leak) senza alcun 502 al client (
|
|
303
|
+
// socket vivi (leak) senza alcun 502 al client (da revisione).
|
|
304
304
|
try {
|
|
305
305
|
upstream.write(buildUpgradeRequest(req.method, parsed.rest, parsed.search, req.headers, node.token, node.localPort));
|
|
306
306
|
if (head && head.length) upstream.write(head);
|
package/lib/proxy/panel-proxy.js
CHANGED
|
@@ -88,7 +88,7 @@ function stripLocalTokenQuery(search) {
|
|
|
88
88
|
// del nostro dominio — inoltrarli significherebbe consegnare al container la
|
|
89
89
|
// sessione del control plane, esattamente cio' che l'Authorization rimossa
|
|
90
90
|
// doveva impedire. E `x-forwarded-*` da un client sono valori che un pannello
|
|
91
|
-
// potrebbe credere veri. Rilievo di
|
|
91
|
+
// potrebbe credere veri. Rilievo di una revisione indipendente.
|
|
92
92
|
function isStrippedRequestHeader(key, value) {
|
|
93
93
|
if (HOP_BY_HOP.has(key)) return true;
|
|
94
94
|
if (key === 'cookie' || key === 'host') return true;
|
|
@@ -194,7 +194,7 @@ async function resolveTarget(resolveCellPanel, cellId) {
|
|
|
194
194
|
};
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
// IL PATH DEL PANNELLO E' `rest`, TAL QUALE. Il contratto
|
|
197
|
+
// IL PATH DEL PANNELLO E' `rest`, TAL QUALE. Il contratto da revisione (CellPanel): il
|
|
198
198
|
// frame carica /api/panel/<cella> + il pathname del panelUrl, e il browser
|
|
199
199
|
// risolve le sotto-risorse RELATIVE rispetto a quell'origine — al proxy
|
|
200
200
|
// arrivano sempre path assoluti del pannello. Ricomporli col basePath del
|
package/lib/pty/attach.js
CHANGED
|
@@ -6,7 +6,7 @@ const { withUtf8Locale } = require('../runtime/env.js');
|
|
|
6
6
|
|
|
7
7
|
// Opens `tmux attach` inside a real PTY, non-destructive for other clients.
|
|
8
8
|
//
|
|
9
|
-
// Size model (emendamento
|
|
9
|
+
// Size model (emendamento di revisione — deck multi-finestra): la sessione usa
|
|
10
10
|
// `window-size latest` (impostato dal bridge). Un client "possiede" la geometria
|
|
11
11
|
// SOLO se NON è `ignore-size`. I tile grid/deck attaccano `ignore-size` (non
|
|
12
12
|
// contendono la geometria da N finestre/N ResizeObserver); l'owner è il tile col
|
package/lib/server.js
CHANGED
|
@@ -93,7 +93,7 @@ function uiBuildVersion(distDir) {
|
|
|
93
93
|
function createServer(opts = {}) {
|
|
94
94
|
const cfg = loadConfig(opts);
|
|
95
95
|
assertLoopback(cfg.bind);
|
|
96
|
-
// Token holder LIVE (
|
|
96
|
+
// Token holder LIVE (da revisione di sicurezza): requireToken/verify leggono tokenStore.get()
|
|
97
97
|
// ad ogni richiesta, cosi' una rotazione via Settings API invalida il VECCHIO token
|
|
98
98
|
// (401) e attiva il NUOVO (200) SENZA restart. Prima il token era catturato una volta
|
|
99
99
|
// allo startup e restava valido fino al restart manuale.
|
|
@@ -136,7 +136,7 @@ function createServer(opts = {}) {
|
|
|
136
136
|
// nei test NON scrivono mai nella home reale. Tutto lazy: vapid.json/asks.json
|
|
137
137
|
// nascono al primo uso, non allo startup.
|
|
138
138
|
const notifyDir = cfg.notifyDir || path.dirname(cfg.tokenPath);
|
|
139
|
-
// READONLY come floor anche dentro il push service (
|
|
139
|
+
// READONLY come floor anche dentro il push service (da revisione): niente generazione
|
|
140
140
|
// VAPID ne' cleanup subscription quando il server e' readonly.
|
|
141
141
|
const bridgeReadonly = () => (cfg.readonlyDefault === true || process.env.NEXUSCREW_READONLY === '1');
|
|
142
142
|
const eventsHub = createEventsHub();
|
|
@@ -585,7 +585,7 @@ function createServer(opts = {}) {
|
|
|
585
585
|
if (!reverseSlotListeners.respond(req, res)) res.status(404).json({ error: 'reverse slot non disponibile' });
|
|
586
586
|
});
|
|
587
587
|
|
|
588
|
-
// Pannello per-cella (
|
|
588
|
+
// Pannello per-cella (da revisione): inoltra il traffico verso il `panelUrl` di UNA cella
|
|
589
589
|
// LOCALE. La destinazione non arriva mai dal chiamante — si risolve dallo stato
|
|
590
590
|
// della cella — e il token di NexusCrew non prosegue verso il pannello, che e'
|
|
591
591
|
// un servizio terzo e non un nodo. Local-only come /api/live-host: il pass-through
|
|
@@ -401,7 +401,7 @@ function createPairHandler(deps) {
|
|
|
401
401
|
});
|
|
402
402
|
}
|
|
403
403
|
|
|
404
|
-
//
|
|
404
|
+
// da revisione: il JOIN ha rivelato la porta pannello del peer — l'informazione
|
|
405
405
|
// che al momento dell'add NON c'era. La riga authorized_keys con DUE
|
|
406
406
|
// destinazioni la emette ADESSO, qui, CHI ha l'informazione: chi ha già
|
|
407
407
|
// incollato la riga di una destinazione è rotto e non lo sa (il server
|
package/lib/settings/routes.js
CHANGED
|
@@ -154,7 +154,7 @@ function settingsRoutes(deps = {}) {
|
|
|
154
154
|
const seams = cfg.settingsSeams || {};
|
|
155
155
|
const localHostname = typeof seams.hostname === 'function' ? seams.hostname : os.hostname;
|
|
156
156
|
// tokenStore/closeSessions iniettati da server.js per la semantica di invalidazione
|
|
157
|
-
// live (
|
|
157
|
+
// live (da revisione di sicurezza). Assenti nei test unitari puri su routes -> la rotazione
|
|
158
158
|
// scrive solo il file (come prima), senza reload in-memory.
|
|
159
159
|
const tokenStore = deps.tokenStore || null;
|
|
160
160
|
const closeSessions = deps.closeSessions || null;
|
|
@@ -164,7 +164,7 @@ function settingsRoutes(deps = {}) {
|
|
|
164
164
|
// configJsonPath() rispetta NEXUSCREW_CONFIG_FILE (stessa risoluzione di
|
|
165
165
|
// loadConfig): settings API e server DEVONO leggere/scrivere lo STESSO file —
|
|
166
166
|
// il fallback home-based divergeva dalla config del server nelle istanze
|
|
167
|
-
// isolate via env (bug trovato in
|
|
167
|
+
// isolate via env (bug trovato in revisione: smoke test che scrivono la config reale).
|
|
168
168
|
const configPath = cfg.configPath || configJsonPath();
|
|
169
169
|
const nodesPath = deps.nodesPath || cfg.nodesPath || nodesStore.defaultNodesPath(home);
|
|
170
170
|
const aliasesPath = deps.aliasesPath || cfg.aliasesPath || nodeAliases.defaultAliasesPath(home);
|
|
@@ -360,7 +360,7 @@ function settingsRoutes(deps = {}) {
|
|
|
360
360
|
|
|
361
361
|
// --- POST /token/rotate — atomico + reload live + chiusura WS; token MAI in risposta
|
|
362
362
|
// Contratto §4b(3): "scrittura atomica del token file + chiusura delle sessioni
|
|
363
|
-
// WS/API attive locali + reload credenziali proxy".
|
|
363
|
+
// WS/API attive locali + reload credenziali proxy". Da revisione: prima questa route
|
|
364
364
|
// scriveva solo il file, e il server teneva il VECCHIO token in memoria -> restava
|
|
365
365
|
// accettato fino al restart manuale. Ora: (1) scrittura atomica, (2) reload live
|
|
366
366
|
// dell'holder in memoria (requireToken/verify vedono il nuovo), (3) chiusura delle
|
|
@@ -548,7 +548,7 @@ function settingsRoutes(deps = {}) {
|
|
|
548
548
|
});
|
|
549
549
|
|
|
550
550
|
// --- POST /nodes/:name/up|down|restart — lifecycle tunnel ------------------
|
|
551
|
-
// GATE READONLY (
|
|
551
|
+
// GATE READONLY (da revisione, contract §4b(6): "restart processi tunnel" e' tra i
|
|
552
552
|
// mutanti della lista chiusa e NEXUSCREW_READONLY blocca TUTTI i mutanti). Prima
|
|
553
553
|
// questi endpoint erano esplicitamente NON gated ("decisione B0") — ma cio'
|
|
554
554
|
// contradiceva il contratto duro §4b(6): up/down/restart mutano processi e vanno
|
package/lib/tmux/lifecycle.js
CHANGED
|
@@ -4,7 +4,7 @@ const path = require('node:path');
|
|
|
4
4
|
const { execFile } = require('node:child_process');
|
|
5
5
|
const { alternateScreenArgs, minimalEnv, tmuxExec } = require('../fleet/launch.js');
|
|
6
6
|
|
|
7
|
-
// Preset allowlistati (
|
|
7
|
+
// Preset allowlistati (da revisione): il client sceglie un NOME, mai un comando.
|
|
8
8
|
// Estendibili da config.json `sessionPresets` (name -> array argv di stringhe).
|
|
9
9
|
const PRESETS = { shell: null, claude: ['claude'], 'codex-vl': ['codex-vl'], pi: ['pi'] };
|
|
10
10
|
|
|
@@ -13,7 +13,7 @@ function validSessionName(name) {
|
|
|
13
13
|
return typeof name === 'string' && NAME_RE.test(name) && !name.startsWith('-');
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
// cwd reale sotto la home reale (
|
|
16
|
+
// cwd reale sotto la home reale (da revisione): realpath su ENTRAMBI, così un
|
|
17
17
|
// symlink dentro home che punta fuori viene rifiutato.
|
|
18
18
|
function resolveCwd(cwd, home) {
|
|
19
19
|
try {
|
|
@@ -25,7 +25,7 @@ function resolveCwd(cwd, home) {
|
|
|
25
25
|
} catch (_) { return null; }
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
// Denylist kill INDIPENDENTE dal registry (
|
|
28
|
+
// Denylist kill INDIPENDENTE dal registry (da revisione): qualunque cloud-* è
|
|
29
29
|
// protetta anche con fleet assente/rotto; in più le tmuxSession del registry.
|
|
30
30
|
function isProtectedSession(name, isCellSession) {
|
|
31
31
|
if (/^cloud-/i.test(String(name))) return true;
|
|
@@ -73,7 +73,7 @@ async function createSession(tmuxBin, { name, cwd, preset }, {
|
|
|
73
73
|
if (typeof ensureProtection === 'function') await ensureProtection();
|
|
74
74
|
await new Promise((resolve, reject) => {
|
|
75
75
|
if (!validSessionName(name)) return reject(httpError(400, 'nome sessione non valido'));
|
|
76
|
-
// Il namespace cloud-* e' delle celle fleet (
|
|
76
|
+
// Il namespace cloud-* e' delle celle fleet (da revisione finale): una generica
|
|
77
77
|
// con quel prefisso occuperebbe il binding per-nome e sarebbe poi 409 al kill.
|
|
78
78
|
if (/^cloud-/i.test(name)) return reject(httpError(409, 'namespace riservato alle celle (cloud-*): usa fleet up'));
|
|
79
79
|
const real = resolveCwd(String(cwd || home), home);
|
package/lib/tmux/preview.js
CHANGED
|
@@ -49,7 +49,7 @@ function analyzeCapture(raw) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// Sampler con cache per sessione, concorrenza limitata e timeout: la preview è
|
|
52
|
-
// best-effort — errori → null, MAI nel log (
|
|
52
|
+
// best-effort — errori → null, MAI nel log (da revisione).
|
|
53
53
|
function createPreviewSampler(tmuxBin, { ttlMs = 3000, timeoutMs = 1500, maxConcurrent = 4 } = {}) {
|
|
54
54
|
const cache = new Map(); // session -> {at, value}
|
|
55
55
|
let inFlight = 0;
|
package/lib/update/runner.js
CHANGED
|
@@ -10,7 +10,7 @@ const {
|
|
|
10
10
|
stableRuntimeDir,
|
|
11
11
|
} = require('./core.js');
|
|
12
12
|
|
|
13
|
-
//
|
|
13
|
+
// da revisione — rigenerazione delle definizioni di boot dopo l'installazione.
|
|
14
14
|
//
|
|
15
15
|
// L'aggiornamento del pacchetto sposta i path versionati che le unit/plist
|
|
16
16
|
// scrivono LETTERALMENTE (node del Cellar su Homebrew, pacchetto in
|
|
@@ -28,7 +28,7 @@ const {
|
|
|
28
28
|
// Contratto: sana SOLO le definizioni che ESISTONO già (il boot è opt-in:
|
|
29
29
|
// l'update non installa un companion mai chiesto); attivazione differita
|
|
30
30
|
// (activate:false) perché il companion RunAtLoad/oneshot non va eseguito a
|
|
31
|
-
// metà update —
|
|
31
|
+
// metà update — rimedio da revisione: l'attivazione la APPLICA healBootDefinitions
|
|
32
32
|
// nel percorso di update (daemon-reload / bootout+bootstrap), non un restart
|
|
33
33
|
// che non carica le definizioni; un fallimento NON blocca l'aggiornamento
|
|
34
34
|
// (il comportamento senza questo passo era proprio quello) ma viene LOGGATO
|
|
@@ -103,7 +103,7 @@ function regenBootDefinitions(opts = {}) {
|
|
|
103
103
|
out.skipped.push(`${target.component}: nessuna definizione installata, nulla da sanare`);
|
|
104
104
|
continue;
|
|
105
105
|
}
|
|
106
|
-
//
|
|
106
|
+
// rimedio da revisione, difetto 3: symlink e directory al posto del target
|
|
107
107
|
// erano già saltati, ma un FILE REGOLARE DI TERZI veniva sovrascritto. Le
|
|
108
108
|
// definizioni che generiamo portano tutte il nome del progetto nell'header
|
|
109
109
|
// (systemd: «# NexusCrew service», plist: Label com.mmmbuto.nexuscrew):
|
|
@@ -111,7 +111,7 @@ function regenBootDefinitions(opts = {}) {
|
|
|
111
111
|
// — lo skip è dichiarato, non silenzioso. Un file illeggibile non blocca
|
|
112
112
|
// la sanazione (si presume nostro: il caso terzi è il contenuto leggibile
|
|
113
113
|
// che dichiara altro).
|
|
114
|
-
//
|
|
114
|
+
// da revisione: la proprieta' si riconosce da un'ancora STRUTTURALE del
|
|
115
115
|
// nostro generatore (isOurService/isOurFleetService), non dalla parola
|
|
116
116
|
// «nexuscrew» presente nel file: un unit di terzi che ci nomina in un
|
|
117
117
|
// commento veniva sovrascritto. E cio' che NON si riesce a leggere non si
|
|
@@ -147,7 +147,7 @@ function regenBootDefinitions(opts = {}) {
|
|
|
147
147
|
return out;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
//
|
|
150
|
+
// rimedio da revisione, difetto 2: l'attivazione differita dichiarata dalla
|
|
151
151
|
// regen era una promessa FALSA in silenzio. `restart` fa `systemctl --user
|
|
152
152
|
// restart` SENZA daemon-reload (systemd riavvia con la definizione VECCHIA
|
|
153
153
|
// in memoria) e `launchctl kickstart` riusa la definizione già caricata; col
|
|
@@ -365,9 +365,9 @@ async function runUpdate(opts = {}) {
|
|
|
365
365
|
installedNew = true;
|
|
366
366
|
await preflightImpl({ version, home });
|
|
367
367
|
writeState(statusPath, { ...readState(statusPath), phase: 'restarting', updaterPid: process.pid, lastError: '' });
|
|
368
|
-
//
|
|
368
|
+
// da revisione: sana le definizioni di boot PRIMA del riavvio — è il passo che
|
|
369
369
|
// manca al percorso di upgrade e che lasciava la companion puntata a un
|
|
370
|
-
// binario morto (exit 78, silenzio).
|
|
370
|
+
// binario morto (exit 78, silenzio). rimedio da revisione: la sanazione ora
|
|
371
371
|
// APPLICA anche l'attivazione differita (healBootDefinitions) — il
|
|
372
372
|
// restart da solo non caricava le definizioni riscritte. Best-effort
|
|
373
373
|
// DICHIARATO: un errore qui non blocca l'aggiornamento, viene loggato e
|
|
@@ -391,9 +391,9 @@ async function runUpdate(opts = {}) {
|
|
|
391
391
|
});
|
|
392
392
|
if (String(readInstalledVersion() || '') !== previousVersion) throw new Error(`rollback verify: attesa ${previousVersion}`);
|
|
393
393
|
await preflightImpl({ version: previousVersion, home, rollback: true });
|
|
394
|
-
//
|
|
394
|
+
// da revisione: il rollback riparte dalle stesse definizioni sane — il
|
|
395
395
|
// downgrade reinstalla il pacchetto nel prefix attuale e i path
|
|
396
|
-
// vivi valgono anche per la versione precedente.
|
|
396
|
+
// vivi valgono anche per la versione precedente. rimedio da revisione:
|
|
397
397
|
// anche qui sanazione+attivazione (heal), non la sola regen.
|
|
398
398
|
(opts.healBootImpl || healBootDefinitions)({
|
|
399
399
|
home, running: wasRunning, execImpl, ...(opts.regenSeams || {}),
|