@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
@@ -11,7 +11,7 @@
11
11
  <meta name="apple-mobile-web-app-title" content="NexusCrew" />
12
12
  <link rel="manifest" href="/manifest.json" />
13
13
  <title>NexusCrew</title>
14
- <script type="module" crossorigin src="/assets/index-ToJ5arRI.js"></script>
14
+ <script type="module" crossorigin src="/assets/index-jBYBPXhB.js"></script>
15
15
  <link rel="stylesheet" crossorigin href="/assets/index-CprIQlC5.css">
16
16
  </head>
17
17
  <body>
@@ -1 +1 @@
1
- {"version":"0.9.15"}
1
+ {"version":"0.9.17"}
@@ -2,7 +2,7 @@
2
2
  // lib/audio/receipt.js — receipt degli enunciati, bounded e ridotto all'osso.
3
3
  //
4
4
  // Scope del chiamante = NODO + CELLA. Non la sola cella: due nodi possono avere
5
- // celle omonime (`Dev` esiste su piu' installazioni), e una chiave basata solo
5
+ // celle omonime (`Alpha` esiste su piu' installazioni), e una chiave basata solo
6
6
  // sul nome permetterebbe a un nodo di leggere o sovrascrivere i receipt di una
7
7
  // cella altrui con lo stesso nome. La chiave e' costruita qui, dal server, mai
8
8
  // accettata dal client.
@@ -8,7 +8,7 @@ function bearerFrom(req) {
8
8
 
9
9
  // 401 uniforme: nessun dettaglio su cosa è andato storto.
10
10
  // `token` puo' essere una stringa (storico) o un holder live {get}: l'holder si
11
- // rilegge ad OGNI richiesta, cosi' una rotazione (audit F7 / §4b(3)) invalida il
11
+ // rilegge ad OGNI richiesta, cosi' una rotazione (da revisione di sicurezza) invalida il
12
12
  // vecchio token senza restart. backward-compat: la stringa resta accettata.
13
13
  function requireToken(token) {
14
14
  const read = (token && typeof token === 'object' && typeof token.get === 'function')
@@ -68,7 +68,7 @@ function validIdentity(value) {
68
68
  && isValidSession(value.tmuxSession);
69
69
  }
70
70
 
71
- function cellsRoutes({ fleetP, instanceId, submit, readonly = () => false, now = () => Date.now() }) {
71
+ function cellsRoutes({ fleetP, instanceId, submit, readonly = () => false, now = () => Date.now(), diagnostics = null }) {
72
72
  const r = express.Router();
73
73
 
74
74
  async function status() {
@@ -96,31 +96,47 @@ function cellsRoutes({ fleetP, instanceId, submit, readonly = () => false, now =
96
96
  r.post('/send', express.json({ limit: '16kb' }), async (req, res) => {
97
97
  if (readonly()) return res.status(403).json({ error: 'READONLY: invio cella bloccato' });
98
98
  const body = req.body || {};
99
+ // Observability (2026-08-28): chi manda, a chi, con quale id e come finisce.
100
+ // SOLO metadati: il testo del messaggio non tocca mai la diagnostica.
101
+ const logSend = (level, event, reason) => {
102
+ if (!diagnostics) return;
103
+ try {
104
+ diagnostics.record(level, 'cell-msg', event, reason
105
+ ? `Invio cella: ${reason}`
106
+ : 'Invio cella consegnato alla sessione target', {
107
+ fromCell: body.from && body.from.cell,
108
+ toCell: body.to && body.to.cell,
109
+ msgId: typeof body.id === 'string' ? body.id : undefined,
110
+ reason: reason ? String(reason).slice(0, 48) : undefined,
111
+ });
112
+ } catch (_) {}
113
+ };
114
+ const reject = (code, reason) => { logSend('warn', 'CELL_MESSAGE_REJECTED', reason); return res.status(code).json({ error: reason }); };
99
115
  const keys = Object.keys(body);
100
116
  if (keys.some((key) => !['id', 'from', 'to', 'message'].includes(key))
101
117
  || !MESSAGE_ID_RE.test(String(body.id || ''))
102
118
  || !validIdentity(body.from) || !validIdentity(body.to)
103
119
  || !submitTextOk(body.message)) {
104
- return res.status(400).json({ error: 'messaggio cella non valido' });
120
+ return reject(400, 'messaggio cella non valido');
105
121
  }
106
122
  const localId = instanceId();
107
123
  if (!NODE_ID_RE.test(String(localId || '')) || body.to.instanceId !== localId) {
108
- return res.status(409).json({ error: 'destinazione non appartiene a questo nodo' });
124
+ return reject(409, 'destinazione non appartiene a questo nodo');
109
125
  }
110
126
  const visited = parseVisited(req);
111
127
  if (visited === null || (visited.length && (visited.at(-1) !== localId
112
128
  || body.from.instanceId !== visited[0]))) {
113
- return res.status(403).json({ error: 'identita mittente non verificata' });
129
+ return reject(403, 'identita mittente non verificata');
114
130
  }
115
131
  if (!visited.length && body.from.instanceId !== localId) {
116
- return res.status(403).json({ error: 'mittente remoto senza route autenticata' });
132
+ return reject(403, 'mittente remoto senza route autenticata');
117
133
  }
118
134
  try {
119
135
  const cells = publicCells(await status(), localId, now());
120
136
  const target = cells.find((cell) => cell.cell === body.to.cell
121
137
  && cell.tmuxSession === body.to.tmuxSession);
122
- if (!target) return res.status(404).json({ error: 'cella destinataria sconosciuta' });
123
- if (!target.canReceive) return res.status(409).json({ error: 'cella destinataria non attiva' });
138
+ if (!target) return reject(404, 'cella destinataria sconosciuta');
139
+ if (!target.canReceive) return reject(409, 'cella destinataria non attiva');
124
140
  const label = `${body.from.cell}@${body.from.instanceId.slice(0, 8)}`;
125
141
  // End on printable text even when the source message ends in a newline:
126
142
  // Pi may auto-submit a bracketed paste that ends with LF. NexusCrew owns
@@ -128,8 +144,9 @@ function cellsRoutes({ fleetP, instanceId, submit, readonly = () => false, now =
128
144
  const envelope = `[NexusCrew message ${body.id} from ${label}]\n${body.message}\n[End NexusCrew message]`;
129
145
  const outcome = await submit(target.tmuxSession, envelope, { engine: target.engine });
130
146
  if (!outcome || outcome.submitted !== true) {
131
- return res.status(409).json({ error: outcome?.reason || 'consegna non riuscita' });
147
+ return reject(409, outcome?.reason || 'consegna non riuscita');
132
148
  }
149
+ logSend('notice', 'CELL_MESSAGE_SENT');
133
150
  const at = now();
134
151
  return res.json({
135
152
  id: body.id,
@@ -138,7 +155,7 @@ function cellsRoutes({ fleetP, instanceId, submit, readonly = () => false, now =
138
155
  to: { instanceId: localId, cell: target.cell, tmuxSession: target.tmuxSession },
139
156
  note: 'submitted conferma solo paste+Enter nel TUI, non elaborazione o completamento',
140
157
  });
141
- } catch (e) { return res.status(500).json({ error: String(e.message || e) }); }
158
+ } catch (e) { return reject(500, String(e.message || e).slice(0, 96)); }
142
159
  });
143
160
 
144
161
  r.use((err, _req, res, _next) => {
@@ -27,7 +27,7 @@ const SESSION_LIST_KEYS = ['sessions', 'unmanaged'];
27
27
  // sessioni sotto quei nomi, ma TILE, e ogni tile porta il nome della sessione
28
28
  // tmux. Filtrare tre chiavi su quattro lasciava passare a un peer ristretto
29
29
  // l'esistenza e il nome di ogni cella dell'hub — non il contenuto, ma la mappa.
30
- // Trovato dall'audit indipendente di NC-E, non da questo codice.
30
+ // Trovato da una revisione indipendente, non da questo codice.
31
31
  function filterDecks(decks, scope) {
32
32
  if (!Array.isArray(decks)) return decks;
33
33
  const out = [];
@@ -86,7 +86,7 @@ function declaredTarget(req) {
86
86
  // si aggiunge alla tabella — di un canale aperto che nessuno vede.
87
87
  //
88
88
  // La prima versione elencava sei chiavi esatte (`cell`, `session`,
89
- // `tmuxSession`, `to.cell`, `to.tmuxSession`, `?session`) e l'audit ha mostrato
89
+ // `tmuxSession`, `to.cell`, `to.tmuxSession`, `?session`) e la revisione ha mostrato
90
90
  // il limite: una route dimenticata che chiama il suo bersaglio `cellId` passava
91
91
  // liscia. Si riconosce quindi la FORMA del nome — qualunque chiave che parli di
92
92
  // celle o di sessioni — invece di un elenco che invecchia a ogni sinonimo.
@@ -560,7 +560,7 @@ async function probeNexusCrew(port, token, opts = {}) {
560
560
  // dice che il servizio E' IN PIEDI e che il token locale non vale — e chi deve
561
561
  // spiegare un riavvio ha bisogno di distinguerle, altrimenti dichiara fallito
562
562
  // un riavvio riuscito e manda a cercare un processo morto che e' vivo.
563
- // Rilievo dell'audit: il caso token-assente era gia' coperto, questo no.
563
+ // Rilievo della revisione: il caso token-assente era gia' coperto, questo no.
564
564
  async function probeNexusCrewStatus(port, token, opts = {}) {
565
565
  const fetchImpl = opts.fetchImpl || globalThis.fetch;
566
566
  if (typeof fetchImpl !== 'function') return null;
@@ -932,7 +932,7 @@ function autoUpdateCommand(args, opts = {}) {
932
932
  // stesso file: tmp nella stessa directory, 0600, rename. Un `writeFileSync`
933
933
  // diretto puo' lasciare un config.json troncato se il processo muore a
934
934
  // meta' — e un config.json illeggibile e' un nodo che non riparte, cioe'
935
- // proprio il guasto che questo comando serve a evitare. Rilievo dell'audit.
935
+ // proprio il guasto che questo comando serve a evitare. Rilievo della revisione.
936
936
  try {
937
937
  if (fs.lstatSync(configPath).isSymbolicLink()) {
938
938
  log('autoupdate: config.json e\' un symlink, non lo scrivo');
@@ -975,7 +975,7 @@ function autoUpdateCommand(args, opts = {}) {
975
975
 
976
976
  // update: npm i -g @latest + sanazione definizioni di boot + restart se attivo.
977
977
  // Fallimento npm -> messaggio chiaro, code 1.
978
- // R28-rimedio, difetto 1 (audit): la via manuale attraversa la STESSA
978
+ // rimedio da revisione, difetto 1: la via manuale attraversa la STESSA
979
979
  // sanazione del runner (healBootDefinitions: rigenera le definizioni sul
980
980
  // node vivo e APPLICA l'attivazione differita). Prima update() reinstallava
981
981
  // e riavviava soltanto: chi aggiornava a mano con un node appena cambiato
@@ -1177,7 +1177,7 @@ async function dispatchNodes(rest, flags, opts = {}) {
1177
1177
  if (!cells.length) { log('nodes cells: elenco vuoto — usa `none` se intendi nessuna cella'); return { code: 1 }; }
1178
1178
  return { code: nodesCmds.nodesEdit({ ...opts, log, ref, patch: { cellVisibility: 'selected', cells } }).code };
1179
1179
  }
1180
- // Accesso ai pannelli (D8): dietro un pannello c'e' un browser con sessioni
1180
+ // Accesso ai pannelli (da revisione): dietro un pannello c'e' un browser con sessioni
1181
1181
  // gia' autenticate, e quell'accesso non si revoca cambiando una chiave. Il
1182
1182
  // resto del modello tratta un peer pairato come l'operatore stesso; qui no:
1183
1183
  // default negato, si concede un nodo per volta.
@@ -1407,7 +1407,7 @@ function dispatch(argv, opts = {}) {
1407
1407
  // SENZA TOKEN NON SI PUO' DIRE NIENTE SULLA SALUTE, e soprattutto non si
1408
1408
  // deve dire qualcosa di sbagliato: la sonda fallirebbe per autenticazione e
1409
1409
  // il messaggio incolperebbe la porta o il processo, mandando a cercare dove
1410
- // il problema non e'. Rilievo dell'audit. Il riavvio e' comunque partito,
1410
+ // il problema non e'. Rilievo della revisione. Il riavvio e' comunque partito,
1411
1411
  // quindi non e' un fallimento: e' una verifica che non si e' potuta fare, e
1412
1412
  // si dice cosi'.
1413
1413
  if (!token) {
@@ -1460,8 +1460,8 @@ function dispatch(argv, opts = {}) {
1460
1460
  // processi sulla stessa porta, e il nostro sopravvivrebbe allo stop del
1461
1461
  // servizio. Li' si riferisce e basta.
1462
1462
  //
1463
- // Difetto mio, trovato rileggendo prima dell'audit: la prima stesura
1464
- // chiamava `startPortable` in ogni caso.
1463
+ // Invariante: il ramo managed non chiama `startPortable`; un fallback
1464
+ // locale creerebbe un secondo processo fuori dal controllo del gestore.
1465
1465
  if (result.runtimeOwner === 'managed') {
1466
1466
  log(`restart: il servizio NON risponde su 127.0.0.1:${port} dopo il riavvio.`);
1467
1467
  log(' Il runtime e\' gestito dal servizio di sistema: non avvio un processo accanto.');
@@ -238,7 +238,7 @@ function fleetInstallCommands(platform, target, ctx) {
238
238
  // Install no-symlink + atomic rename (come service.js). execImpl iniettabile per test;
239
239
  // default execFileSync (argv diretto, MAI shell string). Su activation fallita il file
240
240
  // e' PRESERVATO e le failure raccolte per diagnosi (M1: non si ingoia, non si rollback).
241
- // R28: `activate: false` (speculare a installService) scrive la definizione SENZA
241
+ // da revisione: `activate: false` (speculare a installService) scrive la definizione SENZA
242
242
  // toccare il service manager — per chi rigenera a meta' aggiornamento: il companion
243
243
  // e' RunAtLoad/oneshot, un bootstrap adesso eseguirebbe le celle boot:true mentre
244
244
  // il servizio principale sta per essere riavviato. I comandi saltati sono dichiarati
@@ -313,7 +313,7 @@ function installFleetService(platform, content, ctx, {
313
313
 
314
314
  // Gemello di isOurService (service.js): stessa ragione, stessa disciplina —
315
315
  // l'ancora vive accanto al template della companion.
316
- // Stessa disciplina di service.js (R28, terzo giro): la riga identificativa e'
316
+ // Stessa disciplina di service.js (da revisione): la riga identificativa e'
317
317
  // definita una volta e usata SIA dal template SIA dall'ancora, e l'ancora e' la
318
318
  // riga ESATTA — un prefisso lascia passare il servizio di terzi che comincia la
319
319
  // propria Description col nostro nome.
@@ -72,7 +72,7 @@ function readProcessStart(pid) {
72
72
  }
73
73
 
74
74
  // Campi che SOLO writePidfile puo' scrivere: mai dal chiamante via `extra`.
75
- // Misurato (2026-08-17): con lo spread di extra per ultimo,
75
+ // Misurato in revisione: con lo spread di extra per ultimo,
76
76
  // writePidfile(f, pid, cmd, {processStart:'FINTO', attestation:'unsupported'})
77
77
  // vinceva sul valore vero calcolato da probeProcessStart — un chiamante
78
78
  // poteva scrivere un'attestazione INVENTATA. safeExtra la filtra qui, e i
@@ -105,7 +105,7 @@ const RESERVED_META_FIELDS = new Set(['pid', 'cmd', 'startTs', 'uid', 'processSt
105
105
  // controlla quella variabile d'ambiente controlla anche la finestra — non
106
106
  // un confine di sicurezza reale (chi setta l'ambiente del processo ha gia'
107
107
  // vinto), ma va detto: il marker non e' una garanzia piu' forte di questa.
108
- // 3. R-pidfile-5: claimSchemaMarker (sotto) crea il marker al KILL, non solo
108
+ // 3. da revisione: claimSchemaMarker (sotto) crea il marker al KILL, non solo
109
109
  // al write — quindi un pidfile che sta per essere RIMOSSO puo' lasciare
110
110
  // dietro un marker in una directory che magari non ospitera' mai piu' un
111
111
  // runtime. E' INNOCUO: il marker non ha altro effetto che rendere PIU'
@@ -119,7 +119,7 @@ function schemaMarkerPath(pidfilePath) {
119
119
  return path.join(path.dirname(pidfilePath), '.pidfile-schema-v2');
120
120
  }
121
121
 
122
- // R-pidfile-4 (2026-08-17, audit su develop@437d29f): misurato dall'auditor
122
+ // da revisione interna: misurato in revisione
123
123
  // sul frozen — se .pidfile-schema-v2 esiste come DIRECTORY (o symlink, o
124
124
  // qualunque cosa non sia un file regolare), il vecchio hasSchemaMarker
125
125
  // tornava `false` per costruzione (isFile() falso), e ensureSchemaMarker
@@ -192,7 +192,7 @@ function hasSchemaMarker(pidfilePath) {
192
192
  // pidfile e' gia' stato scritto quando arriviamo qui, ed e' lui
193
193
  // l'informazione che conta.
194
194
  //
195
- // R-pidfile-5 (2026-08-17, audit su develop@fa8bd90): questa funzione NON
195
+ // da revisione interna: questa funzione NON
196
196
  // garantisce piu' nulla sul futuro — prima il commento qui diceva "il
197
197
  // prossimo killPidfile lo scoprira' da solo", ed era FALSO: se questa
198
198
  // scrittura fallisce con ENOSPC/EROFS/quota/permesso transitorio, non resta
@@ -215,7 +215,7 @@ function ensureSchemaMarker(pidfilePath) {
215
215
  }
216
216
  }
217
217
 
218
- // R-pidfile-5: LA decisione di sicurezza. checkSchemaMarker (sopra) guarda
218
+ // da revisione: LA decisione di sicurezza. checkSchemaMarker (sopra) guarda
219
219
  // un EVENTO PASSATO — "il marker e' stato scritto" — che puo' fallire in
220
220
  // silenzio senza lasciare traccia (vedi ensureSchemaMarker): il percorso
221
221
  // resta ENOENT, indistinguibile da "mai tentato". claimSchemaMarker sposta
@@ -444,14 +444,14 @@ function killPidfile(p, signal = 'SIGTERM', impl = {}) {
444
444
  // Prima di questo controllo (nascita, aggiunta dopo cmd) il segnale
445
445
  // partiva sul solo cmd, senza verifica di identità, per QUALUNQUE pidfile
446
446
  // senza processStart — vecchio o nuovo, piattaforma capace o no: era
447
- // esattamente il buco piu' largo trovato dall'audit (2026-08-17), perche'
447
+ // esattamente il buco piu' largo trovato in revisione, perche'
448
448
  // "nessuna nascita nel meta" copriva sia "questa macchina non sa attestare"
449
449
  // sia "l'attestazione e' fallita alla creazione per un motivo che non
450
450
  // conosciamo" con lo STESSO comportamento permissivo. Il ramo sotto separa
451
451
  // i due casi guardando cosa writePidfile ha DICHIARATO al momento in cui
452
452
  // contava (probeProcessStart, sopra), non ricalcolando una sonda adesso —
453
453
  // rifarla qui confonderebbe di nuovo "non so attestare" con "non ci sono
454
- // riuscito in QUESTO istante", l'errore di categoria che l'audit ha
454
+ // riuscito in QUESTO istante", l'errore di categoria che la revisione ha
455
455
  // contestato nel primo disegno di questo fix.
456
456
  let unverifiedBirth = false;
457
457
  let schemaMarker = null; // calcolato pigro: solo se serve davvero (vedi sotto)
@@ -489,7 +489,7 @@ function killPidfile(p, signal = 'SIGTERM', impl = {}) {
489
489
  // altro probe su PID/cmd (la stessa prova debole con un altro nome): serve
490
490
  // una prova ALTERNATIVA del proprietario — per esempio un runId verificato
491
491
  // contro un endpoint locale autenticato. Non e' implementata: e' un lavoro
492
- // a se', con un suo giro di audit (registrato qui, non eseguito).
492
+ // a se', con un suo giro di revisione dedicato (non eseguito).
493
493
  unverifiedBirth = 'unsupported';
494
494
  } else if (meta.attestation === 'indeterminate') {
495
495
  // Il codice NUOVO (questo, o una versione successiva a questo fix) ha
@@ -497,7 +497,7 @@ function killPidfile(p, signal = 'SIGTERM', impl = {}) {
497
497
  // che non sa classificare come proprieta' della piattaforma (vedi
498
498
  // probeProcessStart sopra) — sa che avrebbe potuto, su questo pid,
499
499
  // adesso, e non ce l'ha fatta. Trattarlo come "la piattaforma non sa
500
- // attestare" sarebbe l'errore di categoria contestato dall'audit:
500
+ // attestare" sarebbe l'errore di categoria contestato in revisione:
501
501
  // un'assenza non e' una proprieta' stabile solo perche' e' comoda da
502
502
  // leggere cosi'. Si rifiuta: il cmd da solo matcha per inclusioni, e non
503
503
  // basta a chi non sa nemmeno perche' la nascita manca QUI. Il pidfile
@@ -512,7 +512,7 @@ function killPidfile(p, signal = 'SIGTERM', impl = {}) {
512
512
  // forma di meta la puo' produrre anche un chiamante non nostro): "nessun
513
513
  // campo" da solo non prova la provenienza.
514
514
  //
515
- // R-pidfile-5: la domanda che decide non e' piu' "il marker e' gia'
515
+ // da revisione: la domanda che decide non e' piu' "il marker e' gia'
516
516
  // stato scritto?" (un evento passato che puo' fallire in silenzio —
517
517
  // vedi ensureSchemaMarker/claimSchemaMarker sopra) ma "posso scriverlo
518
518
  // ADESSO?" — claimSchemaMarker prova a crearlo in questo esatto istante:
@@ -534,9 +534,9 @@ function killPidfile(p, signal = 'SIGTERM', impl = {}) {
534
534
  // scrivere qui, verificato nell'istante in cui
535
535
  // conta, non dedotto da uno passato. Si rifiuta.
536
536
  //
537
- // MISURATO (2026-08-17): trattare OGNI meta senza attestazione come
537
+ // MISURATO IN REVISIONE: trattare OGNI meta senza attestazione come
538
538
  // indeterminate rompe l'aggiornamento automatico per ogni nodo il cui
539
- // runtime e' ancora precedente a quando processStart e' nato (8fe514f,
539
+ // runtime e' ancora precedente a quando processStart e' nato (introdotto in
540
540
  // v0.9.0) — npm install sovrascrive pidfile.js PRIMA che il runner lo
541
541
  // richieda, il runner legge il pidfile VECCHIO del runtime in esecuzione,
542
542
  // killPidfile rifiuta, stopPortableRuntime torna killed:false,
@@ -555,9 +555,9 @@ function killPidfile(p, signal = 'SIGTERM', impl = {}) {
555
555
  // morire (il difetto gia' visto sul campo che l'ingresso unico di
556
556
  // riavvio doveva chiudere); un ramo "legacy" permanente basato sulla sola
557
557
  // FORMA del meta (lasciava aperti per sempre restore da backup,
558
- // corruzione e downgrade — l'auditor l'ha contestato); un marker
559
- // controllato ma mai riprovato al kill (questo giro — R-pidfile-5:
560
- // l'auditor ha dimostrato che una scrittura fallita in silenzio riapre
558
+ // corruzione e downgrade — la revisione l'ha contestata); un marker
559
+ // controllato ma mai riprovato al kill (da revisione:
560
+ // una scrittura fallita in silenzio riapre
561
561
  // lo stesso buco senza lasciare traccia).
562
562
  schemaMarker = (impl.claimSchemaMarkerImpl || claimSchemaMarker)(p);
563
563
  if (schemaMarker.state === 'created') {
@@ -407,7 +407,7 @@ function installCommands(platform, target, ctx) {
407
407
  }
408
408
 
409
409
 
410
- // --- Riconoscimento di PROPRIETA' (R28, audit rev2) -------------------------
410
+ // --- Riconoscimento di PROPRIETA' (da revisione) -------------------------
411
411
  // La sanazione automatica riscrive una definizione solo se e' NOSTRA. La prima
412
412
  // versione cercava /nexuscrew/i nel contenuto: un unit di terzi che ci nomina
413
413
  // in un commento («Avviato dopo NexusCrew») veniva sovrascritto. La parola non
@@ -426,7 +426,7 @@ function installCommands(platform, target, ctx) {
426
426
  // dall'ancora: cosi' non possono divergere per costruzione, e il test guardia
427
427
  // diventa una seconda difesa invece dell'unica.
428
428
  //
429
- // R28, terzo giro (audit 2026-08-18): l'ancora linux era un PREFISSO
429
+ // da revisione: l'ancora linux era un PREFISSO
430
430
  // (`/^Description=NexusCrew\b/`) e un servizio di terzi che comincia la sua
431
431
  // Description con il nostro nome — «NexusCrew-compatible proxy», «NexusCrew
432
432
  // fork by tizio» — veniva riconosciuto come nostro e sovrascritto. Il mac era
@@ -109,7 +109,7 @@ function resolveBootPaths({ nodeBin, entryPath, env = process.env, realpath } =
109
109
  };
110
110
  }
111
111
 
112
- // R28: il runner dell'aggiornamento puo' girare con un process.execPath gia'
112
+ // da revisione: il runner dell'aggiornamento puo' girare con un process.execPath gia'
113
113
  // MORTO su disco — l'upgrade di node (Homebrew) unlinka il Cellar vecchio
114
114
  // mentre il processo resta in memoria, e npm reinstalla il pacchetto nel
115
115
  // prefix del node NUOVO. In quel caso resolveBootPaths scriverebbe il path
@@ -4,10 +4,13 @@ const DEFAULT_MAX_RECORDS = 500;
4
4
  const DEFAULT_MAX_BYTES = 256 * 1024;
5
5
  const DEFAULT_MAX_ENTRY_BYTES = 4096;
6
6
  const ALLOWED_DURATIONS = new Set([300, 900, 1800, 3600]);
7
- const LEVELS = new Set(['debug', 'info', 'warn', 'error']);
7
+ const LEVELS = new Set(['debug', 'info', 'notice', 'warn', 'error']);
8
8
  const META_KEYS = new Set([
9
9
  'port', 'platform', 'reason', 'durationSeconds', 'count', 'errno', 'client',
10
10
  'cell', 'engine', 'action', 'state', 'transport', 'phase', 'version', 'status', 'node', 'code',
11
+ // Observability (2026-08-28): metadati di routing e chiusure — identita di
12
+ // cella e id di messaggio, MAI contenuto (DENIED_KEY resta la seconda rete).
13
+ 'fromCell', 'toCell', 'msgId', 'sessionId', 'durationMs', 'gapMs', 'closeCode', 'exitCode', 'drops',
11
14
  ]);
12
15
  const DENIED_KEY = /(authorization|cookie|token|secret|credential|password|prompt|terminal|argv|command|env|content|payload|file|path|endpoint|url)/i;
13
16
 
@@ -47,7 +47,7 @@ function filesRoutes({ cfg, sessionExists, paste, notifier, readonly = () => fal
47
47
  // pasteToSession). Il nome upload e' sanitizzato: se scatta, la causa e'
48
48
  // il filesRoot di configurazione (control char) o un path oltre i 4096
49
49
  // char. Rifiuto noto PRIMA di tmux: la PTY non va neanche tentata.
50
- // 500, non 502 (rilievo audit su d6c47c6): qui il valle NON e' stato
50
+ // 500, non 502 (rilievo da revisione interna): qui il valle NON e' stato
51
51
  // contattato — a rifiutare siamo noi, per configurazione nostra. 502
52
52
  // affermerebbe «il servizio a monte non risponde» e sveglierebbe un
53
53
  // allarme su un tmux che sta benissimo. La coppia di codici e'
@@ -75,7 +75,7 @@ function filesRoutes({ cfg, sessionExists, paste, notifier, readonly = () => fal
75
75
  // nell'outbox della sessione mittente. Path sorgente fail-closed: stringa
76
76
  // assoluta, realpath ESISTENTE sotto HOME (i symlink si risolvono PRIMA del
77
77
  // check: un link che esce da HOME viene rifiutato), file regolare.
78
- // F3 (audit): gated READONLY — la copia e' una scrittura su disco; il gate
78
+ // da revisione: gated READONLY — la copia e' una scrittura su disco; il gate
79
79
  // sta PRIMA di ogni altro check (nessun probe di sessione/path in READONLY).
80
80
  const bridgeReadonly = () => (cfg.readonlyDefault === true || process.env.NEXUSCREW_READONLY === '1');
81
81
  router.post('/outbox', express.json({ limit: '8kb' }), (req, res) => {
@@ -84,23 +84,32 @@ function draftFrom(defs) {
84
84
  };
85
85
  }
86
86
 
87
+ function warnEngineCap(log, engineId, count) {
88
+ const emit = typeof log === 'function' ? log : console.warn;
89
+ emit(`WARN fleet backfill: engine ${engineId} non aggiunto: ${count} engine dichiarati, cap ${CAPS.MAX_ENGINES} raggiunto; riduci gli engine prima di riprovare`);
90
+ }
91
+
87
92
  // Upgrade locale, idempotente e non distruttivo: installazioni gia' esistenti
88
93
  // ricevono l'engine standard Shell senza riscrivere celle o sostituire un id
89
94
  // scelto dall'utente. Se lo store e' pieno o la scrittura non e' possibile, il
90
95
  // bootstrap resta utilizzabile con le definizioni precedenti.
91
96
  function backfillShellEngine(defsPath, defs, log) {
92
97
  if (!defs) return defs;
98
+ const emit = typeof log === 'function' ? log : console.warn;
93
99
  // Le condizioni si valutano su cio' che si legge DENTRO il lock, non
94
100
  // sullo stato che avevamo in mano: e' la differenza fra decidere sul
95
101
  // presente e decidere su una fotografia.
96
102
  const esito = aggiornaDefinizioni(defsPath, (dentro) => {
97
103
  if (dentro.engines.some((engine) => engine.managed?.client === 'shell')) return null;
98
104
  if (dentro.engines.some((engine) => engine.id === 'shell.local')) return null;
99
- if (dentro.engines.length >= CAPS.MAX_ENGINES) return null;
105
+ if (dentro.engines.length >= CAPS.MAX_ENGINES) {
106
+ warnEngineCap(emit, 'shell.local', dentro.engines.length);
107
+ return null;
108
+ }
100
109
  const draft = draftFrom(dentro);
101
110
  draft.engines.push(defaultShellEngine());
102
111
  return draft;
103
- }, { log });
112
+ }, { log: emit });
104
113
  return esito || defs;
105
114
  }
106
115
 
@@ -115,6 +124,7 @@ function backfillShellEngine(defsPath, defs, log) {
115
124
  // avviene solo nel field gate esplicito del coordinator).
116
125
  function backfillAgyEngine(defsPath, defs, cfg = {}) {
117
126
  if (!defs) return defs;
127
+ const emit = typeof cfg.log === 'function' ? cfg.log : console.warn;
118
128
  const platform = cfg.platform || process.platform;
119
129
  const termux = platform === 'android'
120
130
  || termuxRuntimePaths(cfg.env || process.env, { platform, home: cfg.home }) !== null;
@@ -124,11 +134,14 @@ function backfillAgyEngine(defsPath, defs, cfg = {}) {
124
134
  const esito = aggiornaDefinizioni(defsPath, (dentro) => {
125
135
  if (dentro.engines.some((engine) => engine.managed?.client === 'agy')) return null;
126
136
  if (dentro.engines.some((engine) => engine.id === 'agy.native')) return null;
127
- if (dentro.engines.length >= CAPS.MAX_ENGINES) return null;
137
+ if (dentro.engines.length >= CAPS.MAX_ENGINES) {
138
+ warnEngineCap(emit, 'agy.native', dentro.engines.length);
139
+ return null;
140
+ }
128
141
  const draft = draftFrom(dentro);
129
142
  draft.engines.push(defaultAgyEngine());
130
143
  return draft;
131
- }, { log: cfg.log });
144
+ }, { log: emit });
132
145
  return esito || defs;
133
146
  }
134
147
 
@@ -175,7 +188,7 @@ function risolviEseguibile(nome) {
175
188
  function dirScrivibileDaTutti(file) {
176
189
  // Tutta la CATENA, non il solo genitore: con `/a` scrivibile da chiunque,
177
190
  // `/a/b/docker` resta sostituibile rinominando `b` — il binario e la sua
178
- // directory immediata possono essere ineccepibili e il percorso no. Un audit
191
+ // directory immediata possono essere ineccepibili e il percorso no. Una revisione
179
192
  // ha riprodotto esattamente questo caso su un controllo fermo al genitore.
180
193
  //
181
194
  // Lo sticky bit interrompe la risalita per quel livello: li' il kernel
@@ -238,16 +251,20 @@ function defaultDesktopEngine() {
238
251
  // — e' il posto dove qualcuno sa gia' che il container esiste.
239
252
  function backfillDesktopEngine(defsPath, defs, log) {
240
253
  if (!defs) return defs;
254
+ const emit = typeof log === 'function' ? log : console.warn;
241
255
  if (defs.engines.some((engine) => engine.id === 'desktop.local')) {
242
256
  return riparaDesktopEngine(defsPath, defs, log);
243
257
  }
244
258
  const esito = aggiornaDefinizioni(defsPath, (dentro) => {
245
259
  if (dentro.engines.some((engine) => engine.id === 'desktop.local')) return null;
246
- if (dentro.engines.length >= CAPS.MAX_ENGINES) return null;
260
+ if (dentro.engines.length >= CAPS.MAX_ENGINES) {
261
+ warnEngineCap(emit, 'desktop.local', dentro.engines.length);
262
+ return null;
263
+ }
247
264
  const draft = draftFrom(dentro);
248
265
  draft.engines.push(defaultDesktopEngine());
249
266
  return draft;
250
- }, { log });
267
+ }, { log: emit });
251
268
  return esito || defs;
252
269
  }
253
270
 
@@ -322,17 +339,21 @@ function eDaRiparare(defs) {
322
339
  // invariato), rispetta il cap MAX_ENGINES. Non tocca CELLE.
323
340
  function backfillKimiEngine(defsPath, defs, log) {
324
341
  if (!defs) return defs;
342
+ const emit = typeof log === 'function' ? log : console.warn;
325
343
  // Le condizioni si valutano su cio' che si legge DENTRO il lock, non
326
344
  // sullo stato che avevamo in mano: e' la differenza fra decidere sul
327
345
  // presente e decidere su una fotografia.
328
346
  const esito = aggiornaDefinizioni(defsPath, (dentro) => {
329
347
  if (dentro.engines.some((engine) => engine.managed?.client === 'kimi')) return null;
330
348
  if (dentro.engines.some((engine) => engine.id === 'kimi.native')) return null;
331
- if (dentro.engines.length >= CAPS.MAX_ENGINES) return null;
349
+ if (dentro.engines.length >= CAPS.MAX_ENGINES) {
350
+ warnEngineCap(emit, 'kimi.native', dentro.engines.length);
351
+ return null;
352
+ }
332
353
  const draft = draftFrom(dentro);
333
354
  draft.engines.push(defaultKimiEngine());
334
355
  return draft;
335
- }, { log });
356
+ }, { log: emit });
336
357
  return esito || defs;
337
358
  }
338
359
 
@@ -345,6 +366,7 @@ function backfillKimiEngine(defsPath, defs, log) {
345
366
  // MAX_ENGINES. Non tocca CELLE.
346
367
  function backfillGrokEngine(defsPath, defs, cfg = {}) {
347
368
  if (!defs) return defs;
369
+ const emit = typeof cfg.log === 'function' ? cfg.log : console.warn;
348
370
  const platform = cfg.platform || process.platform;
349
371
  const termux = platform === 'android'
350
372
  || termuxRuntimePaths(cfg.env || process.env, { platform, home: cfg.home }) !== null;
@@ -354,11 +376,14 @@ function backfillGrokEngine(defsPath, defs, cfg = {}) {
354
376
  const esito = aggiornaDefinizioni(defsPath, (dentro) => {
355
377
  if (dentro.engines.some((engine) => engine.managed?.client === 'grok')) return null;
356
378
  if (dentro.engines.some((engine) => engine.id === 'grok.native')) return null;
357
- if (dentro.engines.length >= CAPS.MAX_ENGINES) return null;
379
+ if (dentro.engines.length >= CAPS.MAX_ENGINES) {
380
+ warnEngineCap(emit, 'grok.native', dentro.engines.length);
381
+ return null;
382
+ }
358
383
  const draft = draftFrom(dentro);
359
384
  draft.engines.push(defaultGrokEngine());
360
385
  return draft;
361
- }, { log: cfg.log });
386
+ }, { log: emit });
362
387
  return esito || defs;
363
388
  }
364
389
 
@@ -372,17 +397,21 @@ function backfillGrokEngine(defsPath, defs, cfg = {}) {
372
397
  // MAX_ENGINES. Non tocca CELLE.
373
398
  function backfillVlEngine(defsPath, defs, log) {
374
399
  if (!defs) return defs;
400
+ const emit = typeof log === 'function' ? log : console.warn;
375
401
  // Le condizioni si valutano su cio' che si legge DENTRO il lock, non
376
402
  // sullo stato che avevamo in mano: e' la differenza fra decidere sul
377
403
  // presente e decidere su una fotografia.
378
404
  const esito = aggiornaDefinizioni(defsPath, (dentro) => {
379
405
  if (dentro.engines.some((engine) => engine.managed?.client === 'vl')) return null;
380
406
  if (dentro.engines.some((engine) => engine.id === 'vl.native')) return null;
381
- if (dentro.engines.length >= CAPS.MAX_ENGINES) return null;
407
+ if (dentro.engines.length >= CAPS.MAX_ENGINES) {
408
+ warnEngineCap(emit, 'vl.native', dentro.engines.length);
409
+ return null;
410
+ }
382
411
  const draft = draftFrom(dentro);
383
412
  draft.engines.push(defaultVlEngine());
384
413
  return draft;
385
- }, { log });
414
+ }, { log: emit });
386
415
  return esito || defs;
387
416
  }
388
417
 
@@ -557,11 +586,11 @@ async function createBuiltinFleet(cfg = {}) {
557
586
  // passano il flag, quindi la suite esistente non vede endpoint lease aperti da up().
558
587
  const cellLeaseEnabled = cfg.cellLeaseEnabled === true;
559
588
 
560
- // --- P1-2 (audit 3405df0): TUTTI i gate availability AVVENGONO PRIMA di creare
589
+ // --- da revisione interna: TUTTI i gate availability AVVENGONO PRIMA di creare
561
590
  // leaseManager/launchBroker e di leaseManager.boot(). leaseManager non esiste finche'
562
591
  // la Fleet non e' confermata disponibile, quindi NESSUN endpoint lease resta vivo su
563
592
  // un early return (off/blocked) finche' ogni gate resta sopra questa boundary.
564
- // P2 (audit 2a05db2): questo e' un fatto contingente del ref corrente, NON una
593
+ // da revisione interna: questo e' un fatto contingente del ref corrente, NON una
565
594
  // garanzia anti-refactor. Un gate aggiunto DOPO la creazione del manager passerebbe
566
595
  // inosservato senza un controllo statico/mutation-sensitive: il commento non impedisce
567
596
  // un `return off` futuro oltre la boundary. La promessa vera e' limitata: oggi nessun
@@ -576,12 +605,19 @@ async function createBuiltinFleet(cfg = {}) {
576
605
  // fallito, non che ci sia stata un'eccezione.
577
606
  const bootStat = {};
578
607
  let boot = loadDefinitions(defsPath, bootStat);
579
- if (!boot) return bootStat.lstatBlocked
580
- ? { ...off, reason: `fleet.json non verificabile (${bootStat.lstatBlocked}): fail-closed, non "assente" né "invalido"` }
581
- : off;
608
+ if (!boot) {
609
+ if (bootStat.parseReason) {
610
+ const emit = typeof cfg.log === 'function' ? cfg.log : console.warn;
611
+ emit(`WARN fleet definitions: ${bootStat.parseReason}`);
612
+ return { ...off, reason: bootStat.parseReason };
613
+ }
614
+ return bootStat.lstatBlocked
615
+ ? { ...off, reason: `fleet.json non verificabile (${bootStat.lstatBlocked}): fail-closed, non "assente" né "invalido"` }
616
+ : off;
617
+ }
582
618
 
583
619
  if (!readonly()) {
584
- // Audit 093, rilievo 3: lo snapshot della BASE si prende QUI, alla lettura
620
+ // da revisione (rilievo 3): lo snapshot della BASE si prende QUI, alla lettura
585
621
  // che ha prodotto `boot` — non con una rilettura dopo la migrazione. Il
586
622
  // vecchio `primaDelloScrivere` era una rilettura: una scrittura arrivata
587
623
  // DURANTE migrateLegacyTmuxSessions (una catena di chiamate tmux: finestra
@@ -658,8 +694,8 @@ async function createBuiltinFleet(cfg = {}) {
658
694
  // il canale lease del supervisore.
659
695
  ...(leaseManager ? {
660
696
  onLease: (socket, lease) => {
661
- // P1-3 (reaudit dd38c83): attachInitial restituisce false se la
662
- // persistenza del bind fallisce. F-B (audit 2a @ 142e272): ora il
697
+ // da revisione interna: attachInitial restituisce false se la
698
+ // persistenza del bind fallisce. da revisione interna: ora il
663
699
  // broker chiama onLease PRIMA di scrivere il payload. Il ritorno
664
700
  // ESPLICITO false (o la socket distrutta qui) comunica al broker di NON
665
701
  // consegnare il payload: il supervisore riceve la chiusura prima del
@@ -1014,8 +1050,8 @@ async function createBuiltinFleet(cfg = {}) {
1014
1050
  throw httpError(400, 'modello senza id o engine');
1015
1051
  }
1016
1052
  }
1017
- if (!Array.isArray(engines) || engines.length < 1 || engines.length > 24) {
1018
- throw httpError(400, 'engines deve contenere 1..24 definizioni');
1053
+ if (!Array.isArray(engines) || engines.length < 1 || engines.length > CAPS.MAX_ENGINES) {
1054
+ throw httpError(400, `engines deve contenere 1..${CAPS.MAX_ENGINES} definizioni`);
1019
1055
  }
1020
1056
  const allowed = new Set(['id', 'label', 'rc', 'managed', 'command', 'args', 'envKeys', 'model', 'promptMode', 'promptFlag']);
1021
1057
  const seen = new Set();
@@ -140,9 +140,9 @@
140
140
  "supports_search_tool": false
141
141
  },
142
142
  {
143
- "slug": "grok-4.5",
144
- "display_name": "grok-4.5",
145
- "description": "OpenCode Go: grok-4.5",
143
+ "slug": "grok-4.6",
144
+ "display_name": "grok-4.6",
145
+ "description": "OpenCode Go: grok-4.6",
146
146
  "default_reasoning_level": "medium",
147
147
  "supported_reasoning_levels": [
148
148
  {