@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.
- package/CHANGELOG.md +132 -3
- package/docs/ALIBABA_TOKEN_PLAN.md +70 -0
- package/docs/AUDIO_SHARE.md +119 -0
- package/docs/CELL_PANEL.md +156 -0
- package/docs/CONFIGURATION.md +122 -0
- package/docs/FLEET.md +222 -0
- package/docs/INSTALLATION.md +128 -0
- package/docs/LIVE_PROMPT.md +110 -0
- package/docs/MCP.md +151 -0
- package/docs/NODES.md +214 -0
- package/docs/NOTIFICATIONS.md +101 -0
- package/docs/OPERATIONS.md +114 -0
- package/docs/README.md +55 -0
- package/docs/SECURITY.md +185 -0
- package/docs/VL_MICRO_NODES.md +107 -0
- package/docs/img/fleet-deck-desktop.png +0 -0
- package/docs/img/session-mobile.png +0 -0
- package/docs/live-prompt-templates/LIVE_PROMPT.en.md +67 -0
- package/docs/live-prompt-templates/LIVE_PROMPT.es.md +70 -0
- package/docs/live-prompt-templates/LIVE_PROMPT.it.md +67 -0
- package/frontend/dist/assets/{index-ToJ5arRI.js → index-jBYBPXhB.js} +29 -29
- package/frontend/dist/index.html +1 -1
- package/frontend/dist/version.json +1 -1
- package/lib/audio/receipt.js +1 -1
- package/lib/auth/middleware.js +1 -1
- package/lib/cells/routes.js +26 -9
- package/lib/cells/scope-guard.js +2 -2
- package/lib/cli/commands.js +7 -7
- package/lib/cli/fleet-service.js +2 -2
- package/lib/cli/pidfile.js +15 -15
- package/lib/cli/service.js +2 -2
- package/lib/cli/stable-alias.js +1 -1
- package/lib/diagnostics/store.js +4 -1
- package/lib/files/routes.js +2 -2
- package/lib/fleet/builtin.js +59 -23
- package/lib/fleet/catalogs/opencode-go.json +3 -3
- package/lib/fleet/cell-exec.js +4 -4
- package/lib/fleet/cell-lease-server.js +17 -17
- package/lib/fleet/definitions.js +34 -15
- package/lib/fleet/launch-broker.js +3 -3
- package/lib/fleet/lease-client.js +1 -1
- package/lib/fleet/managed.js +97 -26
- package/lib/fleet/prompt-delivery.js +1 -1
- package/lib/fleet/runtime.js +3 -3
- package/lib/live-host/bridge.js +3 -3
- package/lib/live-host/routes.js +1 -1
- package/lib/mcp/server.js +7 -8
- package/lib/mcp/tools.js +1 -1
- package/lib/nodes/commands.js +2 -2
- package/lib/nodes/health.js +7 -11
- package/lib/nodes/identity.js +10 -13
- package/lib/nodes/store.js +2 -2
- package/lib/nodes/tunnel-supervisor.js +7 -7
- package/lib/nodes/tunnel.js +10 -11
- package/lib/notify/asks.js +4 -4
- package/lib/notify/persist.js +1 -1
- package/lib/notify/push.js +4 -4
- package/lib/notify/routes.js +12 -12
- package/lib/proxy/node-proxy.js +4 -4
- package/lib/proxy/panel-proxy.js +2 -2
- package/lib/pty/attach.js +1 -1
- package/lib/server.js +16 -4
- package/lib/settings/pairing-coordinator.js +1 -1
- package/lib/settings/routes.js +4 -4
- package/lib/tmux/lifecycle.js +4 -4
- package/lib/tmux/preview.js +1 -1
- package/lib/update/runner.js +9 -9
- package/lib/vl-nodes/broker.js +1 -1
- package/lib/ws/bridge.js +56 -3
- package/lib/ws/drop-counter.js +37 -0
- package/package.json +20 -2
- package/skills/nexuscrew/SKILL.md +1 -1
- package/frontend/index.html +0 -20
package/lib/fleet/cell-exec.js
CHANGED
|
@@ -19,7 +19,7 @@ const DEFAULT_SUPERVISE = Object.freeze({
|
|
|
19
19
|
maxRapidRestarts: 8,
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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' (
|
|
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 (
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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 (
|
|
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
|
-
//
|
|
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 (
|
|
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 (
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
545
|
-
//
|
|
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
|
-
// (
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
package/lib/fleet/definitions.js
CHANGED
|
@@ -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;
|
|
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 =
|
|
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)
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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)
|
|
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)
|
|
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)
|
|
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.
|
|
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,
|
|
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 (
|
|
107
|
-
// il broker scrive su questo socket (il payload, o dopo
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|