@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.
Files changed (68) hide show
  1. package/CHANGELOG.md +157 -3
  2. package/docs/ALIBABA_TOKEN_PLAN.md +70 -0
  3. package/docs/AUDIO_SHARE.md +119 -0
  4. package/docs/CELL_PANEL.md +156 -0
  5. package/docs/CONFIGURATION.md +122 -0
  6. package/docs/FLEET.md +222 -0
  7. package/docs/INSTALLATION.md +128 -0
  8. package/docs/LIVE_PROMPT.md +110 -0
  9. package/docs/MCP.md +151 -0
  10. package/docs/NODES.md +214 -0
  11. package/docs/NOTIFICATIONS.md +101 -0
  12. package/docs/OPERATIONS.md +114 -0
  13. package/docs/README.md +55 -0
  14. package/docs/SECURITY.md +185 -0
  15. package/docs/VL_MICRO_NODES.md +107 -0
  16. package/docs/img/fleet-deck-desktop.png +0 -0
  17. package/docs/img/session-mobile.png +0 -0
  18. package/docs/live-prompt-templates/LIVE_PROMPT.en.md +67 -0
  19. package/docs/live-prompt-templates/LIVE_PROMPT.es.md +70 -0
  20. package/docs/live-prompt-templates/LIVE_PROMPT.it.md +67 -0
  21. package/frontend/dist/assets/{index-CwsGpQwK.js → index-DmpQoCsc.js} +1 -1
  22. package/frontend/dist/index.html +1 -1
  23. package/frontend/dist/version.json +1 -1
  24. package/lib/auth/middleware.js +1 -1
  25. package/lib/cells/scope-guard.js +2 -2
  26. package/lib/cli/commands.js +7 -7
  27. package/lib/cli/fleet-service.js +2 -2
  28. package/lib/cli/pidfile.js +15 -15
  29. package/lib/cli/service.js +2 -2
  30. package/lib/cli/stable-alias.js +1 -1
  31. package/lib/files/routes.js +2 -2
  32. package/lib/fleet/builtin.js +6 -6
  33. package/lib/fleet/catalogs/zai-responses.json +82 -0
  34. package/lib/fleet/cell-exec.js +4 -4
  35. package/lib/fleet/cell-lease-server.js +17 -17
  36. package/lib/fleet/definitions.js +28 -17
  37. package/lib/fleet/launch-broker.js +3 -3
  38. package/lib/fleet/lease-client.js +1 -1
  39. package/lib/fleet/managed.js +86 -26
  40. package/lib/fleet/prompt-delivery.js +1 -1
  41. package/lib/fleet/runtime.js +3 -3
  42. package/lib/live-host/bridge.js +3 -3
  43. package/lib/live-host/routes.js +1 -1
  44. package/lib/mcp/server.js +7 -8
  45. package/lib/mcp/tools.js +1 -1
  46. package/lib/nodes/commands.js +2 -2
  47. package/lib/nodes/health.js +1 -1
  48. package/lib/nodes/identity.js +10 -13
  49. package/lib/nodes/store.js +2 -2
  50. package/lib/nodes/tunnel-supervisor.js +7 -7
  51. package/lib/nodes/tunnel.js +10 -11
  52. package/lib/notify/asks.js +4 -4
  53. package/lib/notify/persist.js +1 -1
  54. package/lib/notify/push.js +4 -4
  55. package/lib/notify/routes.js +12 -12
  56. package/lib/proxy/node-proxy.js +4 -4
  57. package/lib/proxy/panel-proxy.js +2 -2
  58. package/lib/pty/attach.js +1 -1
  59. package/lib/server.js +3 -3
  60. package/lib/settings/pairing-coordinator.js +1 -1
  61. package/lib/settings/routes.js +4 -4
  62. package/lib/tmux/lifecycle.js +4 -4
  63. package/lib/tmux/preview.js +1 -1
  64. package/lib/update/runner.js +9 -9
  65. package/lib/vl-nodes/broker.js +1 -1
  66. package/lib/ws/bridge.js +1 -1
  67. package/package.json +20 -2
  68. package/frontend/index.html +0 -20
@@ -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 dell'audit indipendente: una funzione provata e senza chiamanti e' il
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
- // La prima stesura tornava `null` per uno schema sbagliato cioe' «genera
83
- // pure una chiave nuova» e LANCIAVA su un JSON corrotto, perche' il parse
84
- // stava fuori dal try. Due esiti opposti per due modi di essere illeggibile,
85
- // e quello comodo era il piu' pericoloso: rigenerare cambia l'identita' di
86
- // questo nodo, e ogni peer che aveva legato la vecchia si ritrova per sempre
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 serve a far accadere la
140
- // corsa in modo deterministico. Senza, il test della corsa e' verde per
141
- // costruzione il file esiste gia' e il percorso di CREAZIONE non viene mai
142
- // eseguito. L'ho scoperto perche' il controllo negativo non falliva.
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);
@@ -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 audit).
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 F1.
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
- // R19 seguito — contratto per la sonda del canale -L quando non conclude MAI:
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 R19 ha tolto, solo con una bandiera "non verificato" appesa sopra).
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
- // R19: connect NON è prova di canale. OpenSSH accetta la TCP sul listener
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
- // R19 punto 3: chi ha installato la chiave quando il pannello non esisteva è
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
- // R19 seguito, secondo difetto (2026-08-17, audit su develop@437d29f):
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 dall'auditor: due fake-ssh vivi dopo un degraded,
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 (R19): il main resta argv/env-driven e NON parte
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 };
@@ -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 (audit F2).
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 (audit F2/F3):
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 (audit F2: prima restituiva started:true e poi l'event 'error' killava Node).
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 (audit F3)
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
- // R19: la riga authorized_keys con le destinazioni EXPLICITE dei -L. Una sola
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
- // Tre giri fa validavo il nome dell'algoritmo, due giri fa la struttura del
719
- // blob, un giro fa la chiedevo a `ssh-keygen -l` sul file `.pub`. Tutte e tre
720
- // le volte un audit ha trovato il caso che restava: e l'ultimo lo chiude solo
721
- // cambiando la domanda. Validare `A.pub` prova che quel file contiene UNA
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 dall'auditor: `null | stringa` comprimeva stati DIVERSI, e il
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
@@ -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 (F2: una sola answer puo' vincere).
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
- // F5: MAX_OPEN e' un cap DURO sugli ask aperti — al cap il nuovo ask viene
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
- // F5: cap duro sugli aperti — rifiuto esplicito, MAI drop di ask aperti.
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 F2 (audit): claim atomico open -> answering ---------------
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
@@ -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 (F4 audit): open
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
@@ -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
- // F7 (audit, threat model SSRF): sendNotification fa una richiesta server-side.
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
- // F3: closure iniettata dal server — in READONLY questo modulo non scrive MAI
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
- // F3: la generazione e' una SCRITTURA — vietata in READONLY (503 esplicito,
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 (F3: riscrivere push.json e' una
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) {
@@ -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 (F3 audit): sono gated 403 TUTTI i mutanti — answer
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 (F1 audit): il campo `session` e' dichiarato dal chiamante e
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 (F5).
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 (F1): entry
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 F1/F5: globale per token (confine di sicurezza) + per
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 R1/rc.14): per la cella mittente e'
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 — F3.
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
- // F7: cap sul numero di subscription -> 429 (quota), input invalido -> 400.
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
- // F3: gated READONLY (mutGate) — crea stato durevole (asks.json) e domande
251
- // che lo stesso server vieterebbe di rispondere. F5: rate-limit creazione
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 (F3: scrittura durevole).
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). F2 (audit): il
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
@@ -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
- // audit: l'origine di una richiesta veniva dedotta dal PATH. Su /api/route il
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 audit).
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 dell'audit settled veniva messo a
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 (audit F5).
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);
@@ -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 un audit indipendente.
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 D8 (CellPanel): il
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 post-audit §5b — deck multi-finestra): la sessione usa
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 (audit F7 / §4b(3)): requireToken/verify leggono tokenStore.get()
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 (F3): niente generazione
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 (D8): inoltra il traffico verso il `panelUrl` di UNA 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
- // R19: il JOIN ha rivelato la porta pannello del peer — l'informazione
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
@@ -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 (audit F7 / §4b(3)). Assenti nei test unitari puri su routes -> la rotazione
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 audit: smoke test che scrivono la config reale).
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". Audit F7: prima questa route
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 (audit F6, contract §4b(6): "restart processi tunnel" e' tra i
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
@@ -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 (audit F1): il client sceglie un NOME, mai un comando.
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 (audit F1): realpath su ENTRAMBI, così un
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 (audit F2): qualunque cloud-* è
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 (audit finale #1): una generica
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);
@@ -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 (audit F7).
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;
@@ -10,7 +10,7 @@ const {
10
10
  stableRuntimeDir,
11
11
  } = require('./core.js');
12
12
 
13
- // R28 — rigenerazione delle definizioni di boot dopo l'installazione.
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 — R28-rimedio: l'attivazione la APPLICA healBootDefinitions
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
- // R28-rimedio, difetto 3 (audit): symlink e directory al posto del target
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
- // R28 (audit rev2): la proprieta' si riconosce da un'ancora STRUTTURALE del
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
- // R28-rimedio, difetto 2 (audit): l'attivazione differita dichiarata dalla
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
- // R28: sana le definizioni di boot PRIMA del riavvio — è il passo che
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). R28-rimedio: la sanazione ora
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
- // R28: il rollback riparte dalle stesse definizioni sane — il
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. R28-rimedio:
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 || {}),