@mmmbuto/nexuscrew 0.9.15 → 0.9.17

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 (73) hide show
  1. package/CHANGELOG.md +132 -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-ToJ5arRI.js → index-jBYBPXhB.js} +29 -29
  22. package/frontend/dist/index.html +1 -1
  23. package/frontend/dist/version.json +1 -1
  24. package/lib/audio/receipt.js +1 -1
  25. package/lib/auth/middleware.js +1 -1
  26. package/lib/cells/routes.js +26 -9
  27. package/lib/cells/scope-guard.js +2 -2
  28. package/lib/cli/commands.js +7 -7
  29. package/lib/cli/fleet-service.js +2 -2
  30. package/lib/cli/pidfile.js +15 -15
  31. package/lib/cli/service.js +2 -2
  32. package/lib/cli/stable-alias.js +1 -1
  33. package/lib/diagnostics/store.js +4 -1
  34. package/lib/files/routes.js +2 -2
  35. package/lib/fleet/builtin.js +59 -23
  36. package/lib/fleet/catalogs/opencode-go.json +3 -3
  37. package/lib/fleet/cell-exec.js +4 -4
  38. package/lib/fleet/cell-lease-server.js +17 -17
  39. package/lib/fleet/definitions.js +34 -15
  40. package/lib/fleet/launch-broker.js +3 -3
  41. package/lib/fleet/lease-client.js +1 -1
  42. package/lib/fleet/managed.js +97 -26
  43. package/lib/fleet/prompt-delivery.js +1 -1
  44. package/lib/fleet/runtime.js +3 -3
  45. package/lib/live-host/bridge.js +3 -3
  46. package/lib/live-host/routes.js +1 -1
  47. package/lib/mcp/server.js +7 -8
  48. package/lib/mcp/tools.js +1 -1
  49. package/lib/nodes/commands.js +2 -2
  50. package/lib/nodes/health.js +7 -11
  51. package/lib/nodes/identity.js +10 -13
  52. package/lib/nodes/store.js +2 -2
  53. package/lib/nodes/tunnel-supervisor.js +7 -7
  54. package/lib/nodes/tunnel.js +10 -11
  55. package/lib/notify/asks.js +4 -4
  56. package/lib/notify/persist.js +1 -1
  57. package/lib/notify/push.js +4 -4
  58. package/lib/notify/routes.js +12 -12
  59. package/lib/proxy/node-proxy.js +4 -4
  60. package/lib/proxy/panel-proxy.js +2 -2
  61. package/lib/pty/attach.js +1 -1
  62. package/lib/server.js +16 -4
  63. package/lib/settings/pairing-coordinator.js +1 -1
  64. package/lib/settings/routes.js +4 -4
  65. package/lib/tmux/lifecycle.js +4 -4
  66. package/lib/tmux/preview.js +1 -1
  67. package/lib/update/runner.js +9 -9
  68. package/lib/vl-nodes/broker.js +1 -1
  69. package/lib/ws/bridge.js +56 -3
  70. package/lib/ws/drop-counter.js +37 -0
  71. package/package.json +20 -2
  72. package/skills/nexuscrew/SKILL.md +1 -1
  73. package/frontend/index.html +0 -20
@@ -19,7 +19,7 @@ const DEFAULT_SUPERVISE = Object.freeze({
19
19
  maxRapidRestarts: 8,
20
20
  });
21
21
 
22
- // F-B (audit 2a, correzione escalation): limite ESPLICITO fra SIGTERM e
22
+ // da revisione (correzione escalation): limite ESPLICITO fra SIGTERM e
23
23
  // SIGKILL quando la lease e' persa (onLost, main()). Un figlio che ignora
24
24
  // SIGTERM non deve restare appeso a tempo indeterminato.
25
25
  const LEASE_LOST_KILL_ESCALATION_MS = 5000;
@@ -303,7 +303,7 @@ async function main(argv = process.argv.slice(2), seams = {}) {
303
303
  // inherit. Il proof (2b) vive nel lease-client, MAI nell'env del child.
304
304
  let generation = 0;
305
305
  let leaseCtl = null;
306
- // F-B (audit 2a): current/stopping vivono QUI (prima del lease-client) perche'
306
+ // da revisione interna: current/stopping vivono QUI (prima del lease-client) perche'
307
307
  // la callback onLost qui sotto li riferisce dalla closure.
308
308
  let current = null; let stopping = false;
309
309
  if (payload.lease && leaseSocket && !leaseSocket.destroyed) {
@@ -315,7 +315,7 @@ async function main(argv = process.argv.slice(2), seams = {}) {
315
315
  // generation += 1). Passiamo un getter cosicche' il reconnect presenti
316
316
  // sempre la generation corrente, non 0 fisso (riconcilia :301 con :381).
317
317
  generation: () => generation,
318
- // F-B (audit 2a @ 142e272): lease persa per tutta la grace senza un
318
+ // da revisione interna: lease persa per tutta la grace senza un
319
319
  // reconnect riuscito. Prima il lease-client desisteva in silenzio dopo
320
320
  // 60s e il child restava un orfano senza lease per tutta la sua vita.
321
321
  // Ora il supervisore viene avvisato: ferma il child (stopping + SIGTERM)
@@ -324,7 +324,7 @@ async function main(argv = process.argv.slice(2), seams = {}) {
324
324
  // Correzione (seconda riconsegna): un SOLO SIGTERM senza escalation
325
325
  // lascia appeso un child che lo ignora (misurato sul percorso reale:
326
326
  // {spawned:1, kills:['SIGTERM'], state:'pending'}) — l'orfano non NASCE
327
- // piu' (F-B originale chiuso davvero), ma un figlio gia' vivo che non
327
+ // piu' (caso originale chiuso davvero), ma un figlio gia' vivo che non
328
328
  // collabora non muore. LEASE_LOST_KILL_ESCALATION_MS e' il limite
329
329
  // ESPLICITO, scritto qui: se il child non e' uscito entro questa
330
330
  // finestra dal SIGTERM, si passa a SIGKILL. `target` fissa il processo
@@ -9,7 +9,7 @@
9
9
  // issuedAt+60s). Il proof supervisore arriva al detentore sul canale lease
10
10
  // (all'attach e a ogni refresh) e non transita mai nel child (R3.1.2 invariato).
11
11
  //
12
- // Storage per-cella (D1/D2): un file per cella in <run>/cell-leases/. Il
12
+ // Storage per-cella (da revisione): un file per cella in <run>/cell-leases/. Il
13
13
  // refresh di una cella rilegge e riscrive SOLO il proprio file: scompare la
14
14
  // read-modify-write dell'intero store condiviso, la race su di essa e
15
15
  // l'amplificazione O(N^2) misurata in 2a (N letture + N scritture complete
@@ -51,7 +51,7 @@ function sanitizeCell(cellId) {
51
51
  const CELL_ID_RE = /^[A-Za-z0-9._-]{1,64}$/;
52
52
  function validCellId(cellId) { return typeof cellId === 'string' && CELL_ID_RE.test(cellId); }
53
53
 
54
- // F-A (audit 2a @ 142e272): il formato che il runtime produce per un'identity.
54
+ // da revisione interna: il formato che il runtime produce per un'identity.
55
55
  // Condiviso da loadPersisted (primo ingresso, al boot) e track (secondo
56
56
  // ingresso): la validazione e' UNA, non due copie da tenere allineate.
57
57
  const EPOCH_RE = /^[a-f0-9]{16}$/;
@@ -112,7 +112,7 @@ function createLeaseManager(cfg = {}, seams = {}) {
112
112
  // IC1.1 (rev28, emendata da rev29): quando il proof nasce da un commit del
113
113
  // bound, issuedAt è DERIVATO da D (D − PROOF_TTL_MS), mai letto dal clock:
114
114
  // expiresAt = issuedAt + TTL è esattamente D, senza toccare verifyProof,
115
- // KIND_FIELDS o wire (vincolo M6 dell'audit rev28).
115
+ // KIND_FIELDS o wire (vincolo della revisione).
116
116
  function issueLeaseProof(cellId, entry, lease, { issuedAt } = {}) {
117
117
  return signProof(ensureVerifier(), {
118
118
  kind: 'lease',
@@ -142,7 +142,7 @@ function createLeaseManager(cfg = {}, seams = {}) {
142
142
  throw e;
143
143
  }
144
144
  const obj = JSON.parse(raw);
145
- // P1-1b: la FORMA del dato. La root deve essere l'entry attesa: plain-object
145
+ // da revisione: la FORMA del dato. La root deve essere l'entry attesa: plain-object
146
146
  // con i campi del formato per-cell. Qualunque altra cosa e' illeggibile.
147
147
  if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
148
148
  throw new Error(`store lease: entry per-cella non e' un oggetto: ${sanitizeCell(cellId)}`);
@@ -211,7 +211,7 @@ function createLeaseManager(cfg = {}, seams = {}) {
211
211
  const prev = entry.graceDeadline;
212
212
  entry.graceDeadline = D;
213
213
  if (!persistEntry(cellId, entry)) {
214
- entry.graceDeadline = prev; // rollback (P1-2b): nessun commit, nessun ACK
214
+ entry.graceDeadline = prev; // rollback (revisione): nessun commit, nessun ACK
215
215
  return null;
216
216
  }
217
217
  return { t, deadline: D, issuedAt: D - PROOF_TTL_MS };
@@ -242,7 +242,7 @@ function createLeaseManager(cfg = {}, seams = {}) {
242
242
  }
243
243
 
244
244
  function bindLiveSocket(cellId, entry, socket, { lease }) {
245
- // P1-2b (audit 2a05db2): persiste il bound PRIMA di associare il socket. Se il
245
+ // da revisione interna: persiste il bound PRIMA di associare il socket. Se il
246
246
  // record non committa, rollback e ritorna false: il caller non dichiarera' lease.
247
247
  // Diverso da EOF (onEOF): li una write fallita lascia un bound ANTERIORE (fail-closed
248
248
  // anticipato, accettabile); qui il bind prometterebbe stato live senza commit durevole.
@@ -314,7 +314,7 @@ function createLeaseManager(cfg = {}, seams = {}) {
314
314
  socket.on('data', onData);
315
315
  socket.once('close', onEOF);
316
316
  socket.once('end', onEOF);
317
- // Correzione (audit 2a, segnalazione precisata in revisione): questo E' il socket
317
+ // Correzione (da revisione, segnalazione precisata): questo E' il socket
318
318
  // su cui il server SCRIVE (l'ack di refresh — proprio dove l'EPIPE nasce se
319
319
  // il peer muore mentre la write e' in volo). Un EventEmitter che emette
320
320
  // 'error' senza listener fa un throw che termina l'INTERO processo — non
@@ -345,7 +345,7 @@ function createLeaseManager(cfg = {}, seams = {}) {
345
345
  socket.setEncoding('utf8');
346
346
  socket.on('data', onData);
347
347
  socket.once('close', () => { try { socket.destroy(); } catch (_) {} });
348
- // Correzione (audit 2a): stesso motivo di bindLiveSocket — il server scrive
348
+ // Correzione (da revisione interna): stesso motivo di bindLiveSocket — il server scrive
349
349
  // 'deny' su questo socket (onData sopra, JSON malformato); senza un
350
350
  // handler 'error' un peer che muore mentre quella write e' in volo manda
351
351
  // in throw fatale l'intero processo. Se la connessione avanza a
@@ -448,7 +448,7 @@ function createLeaseManager(cfg = {}, seams = {}) {
448
448
  const server = netImpl.createServer((sock) => onStableConnection(cellId, sock));
449
449
  await new Promise((resolve, reject) => {
450
450
  server.once('error', reject);
451
- // F-C (audit 2a @ 142e272): niente piu' set/restore di process.umask.
451
+ // da revisione interna: niente piu' set/restore di process.umask.
452
452
  // process.umask e' GLOBALE e la coppia set/restore non e' atomica: sotto
453
453
  // openEndpoint concorrenti il restore di uno ripristinava lo 0o177 appena
454
454
  // settato dall'altro e il processo restava driftato. La protezione e'
@@ -475,7 +475,7 @@ function createLeaseManager(cfg = {}, seams = {}) {
475
475
  return sp;
476
476
  }
477
477
 
478
- // F-C (audit 2a @ 142e272): track() serializzato PER CELLA. Due track
478
+ // da revisione interna: track() serializzato PER CELLA. Due track
479
479
  // simultanei sulla stessa cella vedevano entrambi cells.get() vuoto, generavano
480
480
  // due identity divergenti e il perdente restava con un'identity inutile. La
481
481
  // catena per cellId fa attendere al secondo la fine del primo; poi
@@ -501,7 +501,7 @@ function createLeaseManager(cfg = {}, seams = {}) {
501
501
  // server NON si genera una nuova launchEpoch, perche' il supervisore vivo
502
502
  // reconnecta con quella originale (e il suo proof la porta firmata).
503
503
  const existing = cells.get(cellId);
504
- // P1-2 (reaudit dd38c83): snapshot dell'entry preesistente prima di mutare
504
+ // da revisione interna: snapshot dell'entry preesistente prima di mutare
505
505
  // qualunque cosa. Se persistEntry fallisce su una cella gia' viva, il cleanup
506
506
  // deve ripristinare lo stato precedente, NON cancellarlo.
507
507
  const hadExisting = !!existing;
@@ -513,7 +513,7 @@ function createLeaseManager(cfg = {}, seams = {}) {
513
513
  } : null;
514
514
  // openEndpoint puo' aver creato un nuovo stableServer se l'entry non ne aveva uno.
515
515
  const serverBeforeOpen = existing ? existing.stableServer : null;
516
- // F-A (audit 2a): il SECONDO INGRESSO valida come il primo. Un valore
516
+ // da revisione interna: il SECONDO INGRESSO valida come il primo. Un valore
517
517
  // persistito e' riusato SOLO se ha il formato che il runtime produce
518
518
  // (hex-16 l'epoch); malformato -> identity NUOVA generata, e la persistEntry
519
519
  // qui sotto la scrive, riparando la entry corrotta sul disco.
@@ -541,8 +541,8 @@ function createLeaseManager(cfg = {}, seams = {}) {
541
541
  await openEndpoint(cellId, entry);
542
542
  cells.set(cellId, entry);
543
543
  if (!persistEntry(cellId, entry)) {
544
- // P1-2b: il record durevole (identity) e' essenziale per la recovery post-restart.
545
- // P1-2 (reaudit dd38c83): se la cella era GIA' viva, NON cancellarla — ripristina
544
+ // da revisione: il record durevole (identity) e' essenziale per la recovery post-restart.
545
+ // da revisione interna: se la cella era GIA' viva, NON cancellarla — ripristina
546
546
  // lo stato precedente. Solo le risorse create in QUESTO tentativo vengono pulite.
547
547
  if (hadExisting) {
548
548
  entry.launchEpoch = existingSnapshot.launchEpoch;
@@ -590,7 +590,7 @@ function createLeaseManager(cfg = {}, seams = {}) {
590
590
  // payload il canale appartiene al protocollo del broker (frame length-
591
591
  // prefixed u32): una riga JSON scritta prima del payload corromperebbe la
592
592
  // lettura di receivePayload e il child non nascerebbe — misurato dal gate
593
- // (lease-audit-2a-fixes, F-B positivo). Il proof supervisore arriva con
593
+ // (da revisione interna, esito positivo). Il proof supervisore arriva con
594
594
  // l'ACK del primo refresh, che il lease-client invia IMMEDIATAMENTE
595
595
  // all'avvio: la finestra attach->primo-ack senza proof detenuto e' fail-closed
596
596
  // (reconnect negato -> grace -> onLost), coerente col modello per cui nessun
@@ -628,7 +628,7 @@ function createLeaseManager(cfg = {}, seams = {}) {
628
628
  log(`cell-lease: loadPersisted entry '${cellId}' illeggibile, saltata: ${e && e.message}`);
629
629
  continue;
630
630
  }
631
- // P1-4 (reaudit dd38c83): forma <> semantica. Il runtime produce launchEpoch
631
+ // da revisione interna: forma <> semantica. Il runtime produce launchEpoch
632
632
  // come hex di 16 char. Fail-closed: formato errato = entry saltata.
633
633
  if (!info || typeof info.launchEpoch !== 'string' || !EPOCH_RE.test(info.launchEpoch)) {
634
634
  log(`cell-lease: loadPersisted entry '${cellId}' malformata, saltata`);
@@ -640,7 +640,7 @@ function createLeaseManager(cfg = {}, seams = {}) {
640
640
  cells.set(cellId, {
641
641
  launchEpoch: info.launchEpoch,
642
642
  // IC8.5 (rev29): graceDeadline assente/illeggibile/non-intero = bound trattato come scaduto (0 -> deny sempre).
643
- // P1-4: forma <> semantica — un bound oltre now()+2*GRACE_MS non e' producibile
643
+ // da revisione: forma <> semantica — un bound oltre now()+2*GRACE_MS non e' producibile
644
644
  // da questa fetta: trattato come scaduto (0). Tolleranza 2*GRACE_MS per un
645
645
  // restart durante la grace. Fail-closed: bound assurdo = scaduto.
646
646
  graceDeadline: (Number.isInteger(info.graceDeadline) && info.graceDeadline > 0
@@ -13,9 +13,10 @@ const path = require('node:path');
13
13
  const crypto = require('node:crypto');
14
14
  const { normalizeManagedSpec } = require('./managed.js');
15
15
 
16
- // --- Cap + identita' (dichiarati; ragionevoli per un file di flotta locale) ---
16
+ // --- Cap + identita' (dichiarati; 100 regge una flotta reale con margine;
17
+ // il limite resta deliberato contro un file patologico) ---
17
18
  const SCHEMA_VERSION = 1;
18
- const MAX_ENGINES = 24;
19
+ const MAX_ENGINES = 100;
19
20
  const MAX_CELLS = 32;
20
21
  const MAX_ARGS = 32; // argv: array, mai stringa spezzata (no shell)
21
22
  const MAX_ARG_LEN = 1024; // 1 KB per arg
@@ -204,7 +205,11 @@ function parseModel(m) {
204
205
  // parseDefinitions(raw) -> {schemaVersion, engines, cells, models} | null
205
206
  // Accetta stringa JSON o oggetto gia' parsato. Strict + fail-closed.
206
207
  // ---------------------------------------------------------------------------
207
- function parseDefinitions(raw, { allowLegacyTmuxNames = true } = {}) {
208
+ function parseDefinitions(raw, { allowLegacyTmuxNames = true, onReject = null } = {}) {
209
+ const reject = (message) => {
210
+ if (typeof onReject === 'function') onReject(message);
211
+ return null;
212
+ };
208
213
  try {
209
214
  let d;
210
215
  if (typeof raw === 'string') {
@@ -217,7 +222,9 @@ function parseDefinitions(raw, { allowLegacyTmuxNames = true } = {}) {
217
222
 
218
223
  if (d.schemaVersion !== SCHEMA_VERSION) return null;
219
224
  if (!Array.isArray(d.engines)) return null; // engines obbligatorio (array)
220
- if (d.engines.length > MAX_ENGINES) return null;
225
+ if (d.engines.length > MAX_ENGINES) {
226
+ return reject(`fleet.json rifiutato: ${d.engines.length} engine dichiarati, cap ${MAX_ENGINES}; riduci gli engine a ${MAX_ENGINES} o meno`);
227
+ }
221
228
  if (!Array.isArray(d.cells)) return null; // cells obbligatorio (array)
222
229
  if (d.cells.length > MAX_CELLS) return null;
223
230
 
@@ -226,7 +233,7 @@ function parseDefinitions(raw, { allowLegacyTmuxNames = true } = {}) {
226
233
  // realta' c'e'. `models` e' opzionale: una configurazione che non lo ha
227
234
  // resta valida esattamente com'era.
228
235
  //
229
- // D2 (audit pacchetto): extraModels porta il DESCRITTORE completo (id,
236
+ // da revisione (pacchetto): extraModels porta il DESCRITTORE completo (id,
230
237
  // engine, contextWindow, maxTokens, reasoning), non solo l'id. Prima era
231
238
  // Map<engine, Set<id>>: bastava per il gate "questo id e' ammesso?"
232
239
  // (declaredFor), ma contextWindow/maxTokens/reasoning sparivano — e
@@ -269,7 +276,7 @@ function parseDefinitions(raw, { allowLegacyTmuxNames = true } = {}) {
269
276
  const cellIds = new Set();
270
277
  const cells = [];
271
278
  for (const c of d.cells) {
272
- const cell = parseCell(c, engineIds, engineMap, { allowLegacyTmuxNames, extraModels });
279
+ const cell = parseCell(c, engineIds, engineMap, { allowLegacyTmuxNames, extraModels, onReject: reject });
273
280
  if (!cell) return null;
274
281
  if (cellIds.has(cell.id)) return null; // id cell univoco
275
282
  cellIds.add(cell.id);
@@ -333,7 +340,7 @@ function parseEngine(e, { extraModels = null } = {}) {
333
340
  // pannello, e il fallback engine->cella di cellStatus vale per entrambi.
334
341
  // Prima di questo blocco il campo veniva scartato in silenzio: un valore
335
342
  // valido spariva e uno invalido veniva accettato (fail-open) — rilievo 1
336
- // dell'audit D8.
343
+ // della revisione.
337
344
  let managedPanelUrl;
338
345
  if (e.panelUrl !== undefined) {
339
346
  if (!validPanelUrl(e.panelUrl)) return null;
@@ -419,7 +426,7 @@ function parseEngine(e, { extraModels = null } = {}) {
419
426
  // e' nell'allowlist): un peer poteva mettere un id arbitrario in una cella di
420
427
  // questa installazione, e il boot lo usava senza ricontrollarlo.
421
428
  //
422
- // Rilievo dell'audit indipendente, provato con probe su due profili distinti.
429
+ // Rilievo della revisione indipendente, provato con probe su due profili distinti.
423
430
  // Il controllo e' quello vero, non una copia: si passa dalla stessa
424
431
  // `normalizeManagedSpec` con le stesse dichiarazioni.
425
432
  function cellModelAllowed(engineId, model, engineMap, extraModels) {
@@ -430,7 +437,11 @@ function cellModelAllowed(engineId, model, engineMap, extraModels) {
430
437
  return !!normalizeManagedSpec({ ...engine.managed, model }, { extraModels });
431
438
  }
432
439
 
433
- function parseCell(c, engineIds, engineMap = new Map(), { allowLegacyTmuxNames = true, extraModels = null } = {}) {
440
+ function parseCell(c, engineIds, engineMap = new Map(), { allowLegacyTmuxNames = true, extraModels = null, onReject = null } = {}) {
441
+ const reject = (message) => {
442
+ if (typeof onReject === 'function') onReject(message);
443
+ return null;
444
+ };
434
445
  if (!c || typeof c !== 'object' || Array.isArray(c)) return null;
435
446
 
436
447
  // id
@@ -475,7 +486,9 @@ function parseCell(c, engineIds, engineMap = new Map(), { allowLegacyTmuxNames =
475
486
  if (c.models !== undefined) {
476
487
  if (!c.models || typeof c.models !== 'object' || Array.isArray(c.models)) return null;
477
488
  const entries = Object.entries(c.models);
478
- if (entries.length > MAX_ENGINES) return null;
489
+ if (entries.length > MAX_ENGINES) {
490
+ return reject(`fleet.json rifiutato: la cella ${c.id} contiene ${entries.length} modelli ricordati, cap ${MAX_ENGINES}; riduci la mappa a ${MAX_ENGINES} o meno`);
491
+ }
479
492
  for (const [engineId, value] of entries) {
480
493
  if (!engineIds.has(engineId) || typeof value !== 'string' || !value || value.length > MAX_MODEL_VAL_LEN) return null;
481
494
  // Anche la memoria per-engine passa dal gate: e' un valore che tornera'
@@ -494,7 +507,9 @@ function parseCell(c, engineIds, engineMap = new Map(), { allowLegacyTmuxNames =
494
507
  if (c.permissionPolicies !== undefined) {
495
508
  if (!c.permissionPolicies || typeof c.permissionPolicies !== 'object' || Array.isArray(c.permissionPolicies)) return null;
496
509
  const entries = Object.entries(c.permissionPolicies);
497
- if (entries.length > MAX_ENGINES) return null;
510
+ if (entries.length > MAX_ENGINES) {
511
+ return reject(`fleet.json rifiutato: la cella ${c.id} contiene ${entries.length} permissionPolicies, cap ${MAX_ENGINES}; riduci la mappa a ${MAX_ENGINES} o meno`);
512
+ }
498
513
  permissionPolicies = {};
499
514
  for (const [engineId, value] of entries) {
500
515
  if (!engineIds.has(engineId)) return null;
@@ -511,7 +526,9 @@ function parseCell(c, engineIds, engineMap = new Map(), { allowLegacyTmuxNames =
511
526
  if (c.commands !== undefined) {
512
527
  if (!c.commands || typeof c.commands !== 'object' || Array.isArray(c.commands)) return null;
513
528
  const entries = Object.entries(c.commands);
514
- if (entries.length > MAX_ENGINES) return null;
529
+ if (entries.length > MAX_ENGINES) {
530
+ return reject(`fleet.json rifiutato: la cella ${c.id} contiene ${entries.length} commands, cap ${MAX_ENGINES}; riduci la mappa a ${MAX_ENGINES} o meno`);
531
+ }
515
532
  commands = {};
516
533
  for (const [engineId, value] of entries) {
517
534
  if (!engineIds.has(engineId) || engineMap.get(engineId)?.managed?.client !== 'shell') return null;
@@ -641,7 +658,7 @@ function parseCell(c, engineIds, engineMap = new Map(), { allowLegacyTmuxNames =
641
658
  //
642
659
  // Leggi-modifica-scrivi SOTTO LOCK. `atomicWrite` garantisce che il file non
643
660
  // resti a meta' — non che nessuno lo abbia cambiato mentre lo tenevi in mano:
644
- // sono due proprieta' diverse, e la prima non implica la seconda. Un audit ha
661
+ // sono due proprieta' diverse, e la prima non implica la seconda. Una revisione ha
645
662
  // riprodotto la perdita su SETTE percorsi diversi (i backfill di avvio, la
646
663
  // riparazione desktop e la persistenza della migrazione): ognuno salvava,
647
664
  // osservava sul disco il valore scritto da un altro, e lo perdeva scrivendo un
@@ -871,7 +888,7 @@ function validateCommandTrust(command) {
871
888
  if (!st.isFile()) return { ok: false, reason: 'non e\' un file regolare (symlink o speciale)' }; // lstat: symlink -> isFile()=false
872
889
  if (!(st.mode & 0o100)) return { ok: false, reason: 'non eseguibile dall\'owner' };
873
890
  if (st.mode & 0o002) return { ok: false, reason: 'world-writable' };
874
- // Owner check (design §9a, audit impl #4): il command deve appartenere
891
+ // Owner check (design §9a, da revisione): il command deve appartenere
875
892
  // all'utente del service o a root — un owner terzo potrebbe sostituire
876
893
  // l'eseguibile mantenendo il path "trusted".
877
894
  if (typeof process.getuid === 'function') {
@@ -980,7 +997,9 @@ function loadDefinitions(p, out) {
980
997
  if (st.isSymbolicLink()) return null; // no symlink
981
998
  if (!st.isFile()) return null;
982
999
  const raw = fs.readFileSync(p, 'utf8');
983
- return parseDefinitions(raw);
1000
+ return parseDefinitions(raw, {
1001
+ onReject: out ? (reason) => { out.parseReason = reason; } : null,
1002
+ });
984
1003
  } catch (_) { return null; }
985
1004
  }
986
1005
 
@@ -103,8 +103,8 @@ function createLaunchBroker(cfg = {}) {
103
103
  let raw = '';
104
104
  socket.setEncoding('utf8');
105
105
  socket.setTimeout(3000, () => socket.destroy());
106
- // Correzione (audit 2a, stesso buco trovato in cell-lease-server.js):
107
- // il broker scrive su questo socket (il payload, o dopo F-B onLease
106
+ // Correzione (da revisione, stesso buco trovato in cell-lease-server.js):
107
+ // il broker scrive su questo socket (il payload, o dopo la chiamata onLease
108
108
  // prima). Un peer che muore mentre quella write e' in volo emette
109
109
  // 'error' senza listener -> throw fatale, INTERO processo giu'.
110
110
  socket.once('error', () => { try { socket.destroy(); } catch (_) {} });
@@ -130,7 +130,7 @@ function createLaunchBroker(cfg = {}) {
130
130
  // piu' il timeout idle da 3s pensato per il one-shot.
131
131
  socket.setTimeout(0);
132
132
  socket.removeAllListeners('data');
133
- // F-B (audit 2a @ 142e272): la lease PRIMA del payload. onLease
133
+ // da revisione interna: la lease PRIMA del payload. onLease
134
134
  // associa la connessione al lease manager (attachInitial); solo se
135
135
  // committa il payload viene scritto. Se onLease ritorna false, o ha
136
136
  // gia' distrutto la socket, il payload NON parte: il client riceve
@@ -67,7 +67,7 @@ function startLeaseClient(initialSocket, info, seams = {}) {
67
67
  clearTimer(reconnectTimer);
68
68
  // R3.2: oltre la grace non si ritenta (deny o server muto non vanno in loop).
69
69
  if (reconnectDeadline != null && now() >= reconnectDeadline) {
70
- // F-B (audit 2a @ 142e272): la grace e' scaduta senza un reconnect
70
+ // da revisione interna: la grace e' scaduta senza un reconnect
71
71
  // riuscito: la lease e' PERSA per questo supervisore. Prima si desisteva
72
72
  // in silenzio e il child restava un orfano senza lease per tutta la sua
73
73
  // vita, senza che nessuno lo sapesse. Ora il supervisore viene avvisato