@mmmbuto/nexuscrew 0.9.29 → 0.9.31

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 (78) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/docs/LIVE_PROMPT.md +1 -1
  3. package/docs/NOTIFICATIONS.md +37 -0
  4. package/frontend/dist/assets/index-DaAL7P-m.js +167 -0
  5. package/frontend/dist/assets/{index-CywSos2e.css → index-DqCfZFGi.css} +1 -1
  6. package/frontend/dist/index.html +2 -2
  7. package/frontend/dist/sw.js +91 -10
  8. package/frontend/dist/version.json +1 -1
  9. package/lib/audio/groups.js +1 -1
  10. package/lib/auth/token.js +4 -4
  11. package/lib/cli/commands.js +59 -12
  12. package/lib/cli/doctor.js +36 -7
  13. package/lib/cli/fleet-service.js +7 -7
  14. package/lib/cli/init.js +27 -27
  15. package/lib/cli/pidfile.js +1 -1
  16. package/lib/cli/service.js +6 -6
  17. package/lib/cli/stable-alias.js +4 -4
  18. package/lib/cli/url.js +1 -1
  19. package/lib/config.js +36 -6
  20. package/lib/decks/routes.js +11 -1
  21. package/lib/files/routes.js +1 -1
  22. package/lib/fleet/boot.js +2 -2
  23. package/lib/fleet/builtin.js +28 -19
  24. package/lib/fleet/causes.js +1 -1
  25. package/lib/fleet/cell-exec.js +23 -23
  26. package/lib/fleet/cell-hold.js +6 -6
  27. package/lib/fleet/cell-lease-server.js +106 -106
  28. package/lib/fleet/cell-lease.js +21 -21
  29. package/lib/fleet/definitions.js +21 -13
  30. package/lib/fleet/identity-authority.js +1 -1
  31. package/lib/fleet/launch-broker.js +13 -13
  32. package/lib/fleet/launch.js +15 -15
  33. package/lib/fleet/lease-client.js +23 -23
  34. package/lib/fleet/lease-routes.js +3 -3
  35. package/lib/fleet/lease-verifier.js +22 -22
  36. package/lib/fleet/managed.js +46 -36
  37. package/lib/fleet/prompt-delivery.js +13 -13
  38. package/lib/fleet/routes.js +15 -10
  39. package/lib/fleet/runtime.js +26 -26
  40. package/lib/live-host/bridge.js +36 -36
  41. package/lib/live-host/routes.js +2 -2
  42. package/lib/live-host/store.js +4 -4
  43. package/lib/mcp/server.js +14 -14
  44. package/lib/mcp/tools.js +5 -5
  45. package/lib/nodes/access-presets.js +258 -0
  46. package/lib/nodes/commands.js +198 -4
  47. package/lib/nodes/store.js +152 -9
  48. package/lib/nodes/tunnel.js +7 -7
  49. package/lib/notify/ask-answer-service.js +184 -0
  50. package/lib/notify/ask-receipts.js +197 -0
  51. package/lib/notify/ask-relay.js +152 -0
  52. package/lib/notify/asks.js +28 -3
  53. package/lib/notify/event-feed-acl.js +95 -0
  54. package/lib/notify/event-feed-asks-routes.js +144 -0
  55. package/lib/notify/event-feed-client.js +434 -0
  56. package/lib/notify/event-feed-history.js +115 -0
  57. package/lib/notify/event-feed-producers.js +175 -0
  58. package/lib/notify/event-feed-routes.js +254 -0
  59. package/lib/notify/event-feed.js +328 -0
  60. package/lib/notify/events.js +1 -1
  61. package/lib/notify/notifier.js +3 -3
  62. package/lib/notify/push-relay.js +240 -0
  63. package/lib/notify/push.js +8 -8
  64. package/lib/notify/routes.js +62 -23
  65. package/lib/proxy/federation.js +74 -3
  66. package/lib/proxy/node-proxy.js +13 -13
  67. package/lib/proxy/panel-auth.js +4 -4
  68. package/lib/proxy/panel-proxy.js +2 -2
  69. package/lib/proxy/resource-acl.js +145 -0
  70. package/lib/server.js +372 -39
  71. package/lib/settings/pairing-coordinator.js +1 -1
  72. package/lib/settings/routes.js +45 -32
  73. package/lib/update/manager.js +1 -1
  74. package/lib/update/runner.js +1 -1
  75. package/lib/voice/transcribe.js +1 -1
  76. package/lib/ws/bridge.js +3 -3
  77. package/package.json +1 -1
  78. package/frontend/dist/assets/index-B_THfiL5.js +0 -167
@@ -4,10 +4,10 @@
4
4
  //
5
5
  // Fetta 2b (contratto rev1): l'autenticazione del reconnect e' un proof HMAC
6
6
  // firmato con un verifier PER-INSTALLAZIONE — la capability statica condivisa
7
- // della 2a e' REVOCATA, non affiancata (A2/B1). Solo il server conosce il
8
- // segreto; il supervisore presenta un proof firmato con claims ed expiry (B8:
7
+ // Della 2a e' REVOCATA, non affiancata (/). Solo il server conosce il
8
+ // Segreto; il supervisore presenta un proof firmato con claims ed expiry (
9
9
  // issuedAt+60s). Il proof supervisore arriva al detentore sul canale lease
10
- // (all'attach e a ogni refresh) e non transita mai nel child (R3.1.2 invariato).
10
+ // (on attach and on every refresh) and never transits to the child.
11
11
  //
12
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
@@ -15,22 +15,22 @@
15
15
  // l'amplificazione O(N^2) misurata in 2a (N letture + N scritture complete
16
16
  // per ciclo di 20s). L'invariante di recovery post-restart e' preservato ed
17
17
  // affinato: un file corrotto salta solo la propria cella, non lo store intero
18
- // (E3 chiusa con test). Il nome file usa la stessa sanitizeCell dell'endpoint
18
+ // (closed with tests). Il nome file usa la stessa sanitizeCell dell'endpoint
19
19
  // UDS: due cellId che sanitizzano uguale condividono file ed endpoint, proprieta'
20
20
  // gia' vera in 2a per il socket.
21
21
  //
22
- // Rotazione (C3, sospesa per scelta dichiarata): finche' la finestra di
22
+ // Key rotation (suspended by declared choice): finche' la finestra di
23
23
  // sovrapposizione non e' fissata la chiave NON ruota; la verifica interroga
24
24
  // comunque la LISTA delle chiavi vive (oggi una) perche' quella e' la forma
25
- // richiesta da C2 quando la rotazione sara' attivata. Fail-closed sulla
26
- // verifica (C4); lo stato durevole non contiene mai il segreto, solo
27
- // identificativo e impronta nel meta del verifier (C5); la verifica rende
28
- // osservabile quale chiave ha firmato (C6, via log del keyId).
25
+ // Richiesta da quando la rotazione sara' attivata. Fail-closed sulla
26
+ // Verifica; lo stato durevole non contiene mai il segreto, solo
27
+ // Identificativo e impronta nel meta del verifier; la verifica rende
28
+ // Osservabile quale chiave ha firmato (via log del keyId).
29
29
  //
30
30
  // Side effect isolati e iniettabili (seams) per testabilita', come altrove nel
31
31
  // fleet (cell-exec.js, launch-broker.js). Protocollo sul socket lease:
32
32
  // line-oriented JSON, un messaggio per riga.
33
- // supervisor -> server: {"type":"generation","generation":..} (R1)
33
+ // Supervisor -> server: {"type":"generation","generation":..}
34
34
  // supervisor -> server: {"type":"refresh"}
35
35
  // supervisor -> server: {"type":"reconnect","generation":..,"proof":{..}}
36
36
  // server -> supervisor: {"type":"lease","leaseId":..,"proof":{..}}
@@ -58,7 +58,7 @@ function validCellId(cellId) { return typeof cellId === 'string' && CELL_ID_RE.t
58
58
  // ingresso): la validazione e' UNA, non due copie da tenere allineate.
59
59
  const EPOCH_RE = /^[a-f0-9]{16}$/;
60
60
 
61
- // A3 — confine del consumo: un proof e' consumato UNA volta, IN-PROCESS. Il
61
+ // Confine del consumo: un proof e' consumato UNA volta, IN-PROCESS. Il
62
62
  // registro dei jti muore col processo server: dopo un restart un proof non
63
63
  // ancora scaduto puo' ripresentarsi (la firma e l'expiry restano il gate).
64
64
  // Garanzia piu' forte (single-use cross-restart) NON promessa dal contratto.
@@ -79,12 +79,12 @@ function createLeaseManager(cfg = {}, seams = {}) {
79
79
  // cellId -> entry:
80
80
  // { launchEpoch, stablePath, stableServer, lease, socket, graceTimer, graceDeadline, lastCommit }
81
81
  // lease/socket/lastCommit vivono in memoria (persi al restart); launchEpoch/
82
- // graceDeadline sono anche persistiti PER CELLA (D1). lastCommit è il frutto
83
- // dell'ultimo commitBound riuscito (IC1): t/deadline/issuedAt da cui derivare
82
+ // GraceDeadline sono anche persistiti PER CELLA. lastCommit è il frutto
83
+ // Dell'ultimo commitBound riuscito: t/deadline/issuedAt da cui derivare
84
84
  // i proof senza rileggere il clock. Nessun segreto in entry o su disco.
85
85
  const cells = new Map();
86
86
 
87
- // Verifier per-installazione (B7): file dedicato 0o600 nella runtime dir,
87
+ // Verifier per-installazione: file dedicato 0o600 nella runtime dir,
88
88
  // distinto dai token di liveness e dal segreto del bridge audio. Lazy: nasce
89
89
  // al primo proof, cosi' un manager che non firma mai non lascia file in giro.
90
90
  let verifier = null;
@@ -92,10 +92,10 @@ function createLeaseManager(cfg = {}, seams = {}) {
92
92
  if (!verifier) verifier = loadOrCreateVerifier({ dir, fsImpl: fs, log });
93
93
  return verifier;
94
94
  }
95
- // Le chiavi vive per la verifica (C2-ready, oggi una: C3 sospesa).
95
+ // Le chiavi vive per la verifica (-ready, oggi una: sospesa).
96
96
  const liveKeys = () => [ensureVerifier()];
97
97
 
98
- // Registro jti consumati (A3): bounded, i scaduti escono da soli.
98
+ // Registro jti consumati: bounded, i scaduti escono da soli.
99
99
  const consumedJti = new Map(); // jti -> expiresAt
100
100
  function consumeJti(jti, expiresAt) {
101
101
  const t = now();
@@ -110,10 +110,10 @@ function createLeaseManager(cfg = {}, seams = {}) {
110
110
  return true;
111
111
  }
112
112
 
113
- // Proof supervisore (kind 'lease'): tupla con leaseId (B6), generation
114
- // corrente, expiry issuedAt+60s (B8). Emissione == firma: non c'e' stato di
113
+ // Proof supervisore (kind 'lease'): tupla con leaseId, generation
114
+ // Corrente, expiry issuedAt+60s. Emissione == firma: non c'e' stato di
115
115
  // sessione da tenere, il detentore presenta il proof cosi' com'e'.
116
- // IC1.1 (rev28, emendata da rev29): quando il proof nasce da un commit del
116
+ // Per the amended contract: quando il proof nasce da un commit del
117
117
  // bound, issuedAt è DERIVATO da D (D − PROOF_TTL_MS), mai letto dal clock:
118
118
  // expiresAt = issuedAt + TTL è esattamente D, senza toccare verifyProof,
119
119
  // KIND_FIELDS o wire (vincolo della revisione).
@@ -133,7 +133,7 @@ function createLeaseManager(cfg = {}, seams = {}) {
133
133
 
134
134
  function stablePathFor(cellId) { return path.join(dir, `cell-${sanitizeCell(cellId)}.sock`); }
135
135
 
136
- // D1: lettura PER CELLA. ENOENT = cella sconosciuta (legittimo); qualunque
136
+ // Lettura PER CELLA. ENOENT = cella sconosciuta (legittimo); qualunque
137
137
  // altro errore (EIO, parse, forma) = illeggibile: propaga, perche' una
138
138
  // scrittura che ignora un file illeggibile cancellerebbe lo stato di quella
139
139
  // cella senza saperlo.
@@ -155,7 +155,7 @@ function createLeaseManager(cfg = {}, seams = {}) {
155
155
  }
156
156
 
157
157
  function writePersistedCell(cellId, entryData) {
158
- // IC8.2/IC8.3 (rev29): l'esito NON si ingoia — ritornato perche' il refresh (e il
158
+ // The outcome is never swallowed: l'esito NON si ingoia — ritornato perche' il refresh (e il
159
159
  // proof) dipende dal commit effettivo del bound. Il log resta diagnostico.
160
160
  try {
161
161
  ensureRuntimeDir(dir);
@@ -172,13 +172,13 @@ function createLeaseManager(cfg = {}, seams = {}) {
172
172
  }
173
173
  }
174
174
 
175
- // D1: persiste il bound di UNA cella leggendo e scrivendo SOLO il suo file.
175
+ // Persiste il bound di UNA cella leggendo e scrivendo SOLO il suo file.
176
176
  // Non esiste piu' la read-modify-write dell'intero store condiviso.
177
- // IC8.1 (rev29): graceDeadline SEMPRE valorizzato come intero valido (mai
177
+ // The persisted graceDeadline SEMPRE valorizzato come intero valido (mai
178
178
  // null) — bound durevole per rifiutare reconnect oltre la grace post-restart
179
- // (R3.3.5). Live = now+PROOF_TTL_MS (IC1: l'ancora live e' la vita del
180
- // proof, non la grace); in grace = bound di armGrace (R3.2); refresh lo
181
- // rinfresca.
179
+ // Live = now + PROOF_TTL_MS (the live anchor is the proof lifetime,
180
+ // not the grace); in grace = the armGrace bound; refresh renews it.
181
+
182
182
  function persistEntry(cellId, entry) {
183
183
  return writePersistedCell(cellId, {
184
184
  launchEpoch: entry.launchEpoch,
@@ -186,26 +186,26 @@ function createLeaseManager(cfg = {}, seams = {}) {
186
186
  });
187
187
  }
188
188
 
189
- // IC1 (rev28, come emendata da rev29) — UNA deadline D per (cella,
189
+ // One deadline D per (cell, incarnation): UNA deadline D per (cella,
190
190
  // incarnazione). Il bound persistito e l'expiry del proof NON sono due valori
191
191
  // coordinati: sono LO STESSO VALORE. La transazione legge il clock UNA volta
192
192
  // sola; D = max(D corrente, t + PROOF_TTL_MS) — monotona non decrescente
193
- // (IC1.2); il proof nasce con issuedAt = D − PROOF_TTL_MS, quindi expiresAt
193
+ //; il proof nasce con issuedAt = D − PROOF_TTL_MS, quindi expiresAt
194
194
  // === D per costruzione e verifyProof (expiresAt === issuedAt + TTL) resta
195
195
  // intatto. La persistenza è SINCRONA (writeFileSync + renameSync) e l'intera
196
196
  // transazione update→persist→ACK+proof vive in un solo giro di event loop:
197
- // indivisibile (IC1.3). La serializzazione viene dal runtime (IC4.1/IC9.1) —
197
+ // Indivisibile. La serializzazione viene dal runtime (/) —
198
198
  // dichiarato qui, non assunto altrove.
199
199
  // L'ancora del bound LIVE è la vita del proof (PROOF_TTL_MS), non la grace:
200
- // GRACE_MS resta l'ancora della grace a EOF (R3.2). I due valori oggi
201
- // coincidono (60s): cambia l'ancora semantica, non il numero.
200
+ // GRACE_MS remains the anchor of the EOF grace. The two values currently
201
+ // coincide (60s): the semantic anchor changes, not the number.
202
202
  function commitBound(cellId, entry) {
203
203
  const t = now(); // unica lettura del clock per l'intera transazione
204
204
  const proposed = t + PROOF_TTL_MS;
205
205
  const current = Number.isSafeInteger(entry.graceDeadline) && entry.graceDeadline > 0 ? entry.graceDeadline : 0;
206
- const D = Math.max(current, proposed); // IC1.2: mai all'indietro
206
+ const D = Math.max(current, proposed); // Mai all'indietro
207
207
  if (D <= t) {
208
- // IC1.6: D già scaduta al commit NON è un successo: niente ACK, niente
208
+ // D già scaduta al commit NON è un successo: niente ACK, niente
209
209
  // proof. Con proposed = t + TTL è irraggiungibile per costruzione; la
210
210
  // guardia resta perché vieta l'implementazione alternativa (D derivata
211
211
  // da dati stantii) che il contratto esclude.
@@ -227,7 +227,7 @@ function createLeaseManager(cfg = {}, seams = {}) {
227
227
 
228
228
  function armGraceTimer(cellId, entry) {
229
229
  clearGraceTimer(entry);
230
- // R3.2: deadline non estendibile, armata una sola volta.
230
+ // Non-extendable deadline, armed once.
231
231
  const ms = Math.max(0, (entry.lease.graceDeadline - now()));
232
232
  entry.graceTimer = setTimer(() => {
233
233
  const e = cells.get(cellId);
@@ -250,7 +250,7 @@ function createLeaseManager(cfg = {}, seams = {}) {
250
250
  // record non committa, rollback e ritorna false: il caller non dichiarera' lease.
251
251
  // Diverso da EOF (onEOF): li una write fallita lascia un bound ANTERIORE (fail-closed
252
252
  // anticipato, accettabile); qui il bind prometterebbe stato live senza commit durevole.
253
- // IC1: il commit passa da commitBound — UNA deadline, monotona, ancorata alla
253
+ // Il commit passa da commitBound — UNA deadline, monotona, ancorata alla
254
254
  // vita del proof. entry.lastCommit (SOLO in memoria: il formato per-cella su
255
255
  // disco non cambia) porta t/issuedAt al caller, perché il proof del frame
256
256
  // lease nasca derivato da D senza nuove letture di clock.
@@ -278,12 +278,12 @@ function createLeaseManager(cfg = {}, seams = {}) {
278
278
  return;
279
279
  }
280
280
  if (msg.type === 'refresh') {
281
- // Heartbeat lato supervisore (R3.2) + IC1 (rev28/rev29): il refresh
282
- // committa LA deadline D — monotona, ed è l'expiry del proof che sta
283
- // per nascere — e SOLO se il commit è durevole emette ACK+proof IN UN
284
- // SOLO FRAME (IC1.4). Se la persistenza non committa il refresh NON è
285
- // un successo — nessun ACK, nessun proof (IC8.2/8.4): il detentore
286
- // resta col proof vecchio, che scade: fail-closed per scadenza, non per
281
+ // Supervisor-side heartbeat: the refresh commits THE deadline D — monotonic,
282
+ // and it is the expiry of the proof about to be born — and ONLY if the
283
+ // Commit is durable does it emit ACK+proof IN ONE SINGLE FRAME.
284
+ // If persistence does not commit, the refresh is NOT a success — no ACK,
285
+ // No proof (/8.4): the holder keeps the old proof, which expires:
286
+ // fail-closed by expiry, not by silence.
287
287
  // silenzio.
288
288
  const cur = cells.get(cellId);
289
289
  if (cur && cur.lease && cur.socket === socket) {
@@ -314,13 +314,13 @@ function createLeaseManager(cfg = {}, seams = {}) {
314
314
  const onEOF = () => {
315
315
  const cur = cells.get(cellId);
316
316
  if (!cur || cur.socket !== socket) return; // gia' sostituita da un reconnect
317
- // R3.1.3: EOF arma UNA sola transizione monotonica Live -> Grace; deadline
318
- // non estendibile. armGrace e' no-op se gia' in grace.
317
+ // EOF arms a SINGLE monotonic Live -> Grace transition; deadline
318
+ // not extendable. armGrace is a no-op if already in grace.
319
319
  const g = L.armGrace(cur.lease, { now: now() });
320
320
  if (g) cur.lease = g;
321
- // R3.3.5: persistiamo il nuovo bound di grace per rifiutare reconnect stale
322
- // post-restart (il lease vivo non sopravvive, ma il bound di reject si').
323
- // IC1.2: il bound non arretra nemmeno qui — eof >= ultimo refresh rende la
321
+ // We persist the new grace bound to reject stale reconnects
322
+ // post-restart (the live lease does not survive, but the reject bound does).
323
+ // Il bound non arretra nemmeno qui — eof >= ultimo refresh rende la
324
324
  // grace già monotona in pratica; il max la pinna anche sotto clock ostile.
325
325
  const graceBound = cur.lease && cur.lease.graceDeadline != null ? cur.lease.graceDeadline : now() + L.GRACE_MS;
326
326
  cur.graceDeadline = Math.max(
@@ -400,7 +400,7 @@ function createLeaseManager(cfg = {}, seams = {}) {
400
400
  && validDaemonChallenge(msg.challenge);
401
401
  }
402
402
 
403
- // R1: transizione di generazione della connessione viva. Solo stessa
403
+ // Transizione di generazione della connessione viva. Solo stessa
404
404
  // generazione (idempotente) o +1 (restart del supervisore); generazioni
405
405
  // arbitrarie restano deny -> il relay fail-closed (revoked) non cambia.
406
406
  function validGenerationRequest(msg) {
@@ -484,7 +484,7 @@ function handleChallengeProof(cellId, entry, socket, msg) {
484
484
  // viaggia, ma la decisione resta qui: l'authority ri-calcola l'HMAC, la
485
485
  // finestra temporale, il replay (nonce single-use) e la revoca (jti), e i
486
486
  // claims vengono confrontati col SUBJECT AUTENTICATO del lancio — mai con
487
- // campi del messaggio (C7-bis). Fail-closed: qualunque esito non ok resta
487
+ // Campi del messaggio (-bis). Fail-closed: qualunque esito non ok resta
488
488
  // una negazione motivata, il canale non apre nulla.
489
489
  function handleVerify(cellId, entry, socket, msg) {
490
490
  if (!validVerifyRequest(msg)) {
@@ -548,7 +548,7 @@ function handleChallengeProof(cellId, entry, socket, msg) {
548
548
  }
549
549
 
550
550
  function onStableConnection(cellId, socket) {
551
- // Endpoint stabile: reconnect (R3.3.2-4). Legge identita' + proof, valida.
551
+ // Stable endpoint: reconnect. Reads identity + proof, validates.
552
552
  let buf = '';
553
553
  const onData = (chunk) => {
554
554
  buf += chunk.toString();
@@ -580,10 +580,10 @@ function handleChallengeProof(cellId, entry, socket, msg) {
580
580
  if (!msg || msg.type !== 'reconnect') return denyReconnect(socket);
581
581
  const entry = cells.get(cellId);
582
582
  if (!entry) return denyReconnect(socket);
583
- // A2/B1: l'autenticazione del reconnect e' il proof HMAC. La capability
583
+ // : l'autenticazione del reconnect e' il proof HMAC. La capability
584
584
  // statica della 2a e' revocata: un messaggio senza proof (o col vecchio
585
585
  // campo capability) e' negato qui, senza confronti verso segreti condivisi.
586
- // C4: fail-closed — forma, firma, claims attesi, expiry: ogni difetto e' deny.
586
+ // Fail-closed — forma, firma, claims attesi, expiry: ogni difetto e' deny.
587
587
  const out = verifyProof(liveKeys(), msg.proof, {
588
588
  now,
589
589
  expect: {
@@ -591,7 +591,7 @@ function handleChallengeProof(cellId, entry, socket, msg) {
591
591
  cellId,
592
592
  launchEpoch: entry.launchEpoch,
593
593
  // leaseId atteso SOLO con lease vivo in memoria: post-restart il lease
594
- // non sopravvive (R3.3.1) e il gate resta firma+expiry+bound di grace.
594
+ // does not survive, and the gate stays signature+expiry+grace-bound.
595
595
  ...(entry.lease ? { leaseId: entry.lease.leaseId } : {}),
596
596
  },
597
597
  });
@@ -599,7 +599,7 @@ function handleChallengeProof(cellId, entry, socket, msg) {
599
599
  log(`cell-lease: ${cellId} reconnect denied (proof: ${out.reason})`);
600
600
  return denyReconnect(socket);
601
601
  }
602
- // A3: consumo il jti DOPO la verifica e PRIMA di ogni mutazione. Un proof
602
+ // Consumo il jti DOPO la verifica e PRIMA di ogni mutazione. Un proof
603
603
  // presentato e negato per altro motivo NON viene consumato: potra' ripresentarsi
604
604
  // fino alla propria scadenza, e ogni replica sara' negata dallo stesso gate.
605
605
  if (!consumeJti(msg.proof.jti, msg.proof.expiresAt)) {
@@ -607,25 +607,25 @@ function handleChallengeProof(cellId, entry, socket, msg) {
607
607
  return denyReconnect(socket);
608
608
  }
609
609
  const generation = Number.isInteger(msg.generation) && msg.generation >= 0 ? msg.generation : (entry.lease ? entry.lease.generation : 0);
610
- // R3.3.4: transizione di generation VERIFICABILE (non solo non-decreasing). Il
611
- // supervisore onesto (cell-exec.js) fa avanzare la generation di ESATTAMENTE +1
612
- // a ogni restart del child e al reconnect presenta sempre la propria generation
613
- // corrente. La transizione attesa e' quindi `=== current` (stesso restart,
614
- // retry/reattach) oppure `=== current + 1` (un restart del supervisore). Un salto
615
- // avanti arbitrario (es. 0->99) o all'indietro NON e' una transizione che il
616
- // client onesto produrrebbe: deny. msg.generation assente -> fallback alla
617
- // current (compat), accettato. Post-restart (entry.lease null) non c'e' generation
618
- // persistita da validare (contratto rev25 EC6): firma+bound di grace resta il gate.
610
+ // A VERIFIABLE generation transition (not merely non-decreasing). An honest
611
+ // supervisor (cell-exec.js) advances the generation by EXACTLY +1 on every
612
+ // child restart and always presents its current generation on reconnect.
613
+ // The expected transition is therefore `=== current` (same restart,
614
+ // retry/reattach) or `=== current + 1` (one supervisor restart). An arbitrary
615
+ // jump forward (e.g. 0->99) or backward is NOT a transition an honest
616
+ // client would produce: deny. Missing msg.generation -> fallback to current
617
+ // (compat), accepted. Post-restart (entry.lease null) there is no persisted
618
+ // generation to validate: signature + grace bound stay the gate.
619
619
  if (entry.lease && Number.isInteger(msg.generation)) {
620
620
  const cur = entry.lease.generation;
621
621
  if (msg.generation !== cur && msg.generation !== cur + 1) {
622
622
  return denyReconnect(socket);
623
623
  }
624
624
  }
625
- // R3.3.5/IC8.1 (rev29): oltre la grace il reconnect e' rifiutato. Con entry.lease
626
- // vivo ci pensa reattach (null su grace scaduta). Post-restart (lease null) usiamo
627
- // il bound di grace persistito PER CELLA, ora SEMPRE valorizzato: se la richiesta
628
- // arriva ALLA deadline o oltre, rifiuta (>= allineato a cell-lease.js).
625
+ // Past the grace the reconnect is refused. With a live entry.lease
626
+ // reattach handles it (null on an expired grace). Post-restart (lease null) we
627
+ // use the per-cell persisted grace bound, now ALWAYS set: if the request
628
+ // arrives AT or past the deadline, refuse (>= aligned with cell-lease.js).
629
629
  // Recovery post-restart con supervisore/child vivi solo ENTRO il bound live.
630
630
  if (!entry.lease && now() >= entry.graceDeadline) {
631
631
  return denyReconnect(socket);
@@ -635,12 +635,12 @@ function handleChallengeProof(cellId, entry, socket, msg) {
635
635
  ? L.reattach(entry.lease, { leaseId: L.newLeaseId(), generation, now: now() })
636
636
  : base;
637
637
  if (!reattached) return denyReconnect(socket);
638
- // R3.3.4: lease NUOVO (leaseId nuovo), stessa identita'. Associa la nuova connessione.
638
+ // NEW lease (new leaseId), same identity. Binds the new connection.
639
639
  if (!bindLiveSocket(cellId, entry, socket, { lease: reattached })) {
640
640
  return denyReconnect(socket);
641
641
  }
642
- log(`cell-lease: ${cellId} reconnect ok (lease ${reattached.leaseId}, verifier ${out.keyId})`); // C6: chiave osservabile
643
- // IC1.1: il proof del frame lease nasce DAL commit appena fatto (issuedAt =
642
+ log(`cell-lease: ${cellId} reconnect ok (lease ${reattached.leaseId}, verifier ${out.keyId})`); // Chiave osservabile
643
+ // Il proof del frame lease nasce DAL commit appena fatto (issuedAt =
644
644
  // D − TTL): stessa deadline del bound appena persistito, non una lettura di
645
645
  // clock nuova. bindLiveSocket riuscito implica commit fatto: il ramo senza
646
646
  // proof è difensivo (fail-closed: il detentore resta col proof pregresso,
@@ -653,10 +653,10 @@ function handleChallengeProof(cellId, entry, socket, msg) {
653
653
 
654
654
  // --- API pubblica ---
655
655
 
656
- // Apre l'endpoint stabile UDS 0o600 per una cella (R3.3.2), riusando l'identity
657
- // e lo stablePath gia' noti nell'entry. Idempotente: se l'entry ha gia' uno
658
- // stableServer vivo non fa nulla. Condiviso da track() (prima apertura) e da
659
- // boot() (riapertura dopo restart per le celle persistite).
656
+ // Opens the per-cell stable UDS endpoint 0o600, reusing the entry's known
657
+ // identity and stablePath. Idempotent: if the entry already has a live
658
+ // stableServer it does nothing. Shared by track() (first open) and
659
+ // boot() (reopen after restart for persisted cells).
660
660
  async function openEndpoint(cellId, entry) {
661
661
  if (entry.stableServer) return entry.stablePath;
662
662
  ensureRuntimeDir(dir);
@@ -675,7 +675,7 @@ function handleChallengeProof(cellId, entry, socket, msg) {
675
675
  server.listen(sp, () => {
676
676
  server.removeListener('error', reject);
677
677
  server.unref();
678
- // Il chmod forza/verifica 0o600; il suo fallimento NON va ingoiato (R3.3.2).
678
+ // The chmod enforces/verifies 0o600; its failure must NOT be swallowed.
679
679
  try {
680
680
  fsImpl.chmodSync(sp, 0o600);
681
681
  } catch (e) {
@@ -748,7 +748,7 @@ function handleChallengeProof(cellId, entry, socket, msg) {
748
748
  || crypto.randomBytes(8).toString('hex');
749
749
  const entry = existing
750
750
  || { launchEpoch, stablePath: stablePathFor(cellId), stableServer: null, lease: null, socket: null, graceTimer: null,
751
- // IC1: anche il bound iniziale di una cella nuova è ancorato alla vita
751
+ // Anche il bound iniziale di una cella nuova è ancorato alla vita
752
752
  // del proof (PROOF_TTL_MS), stesso valore di GRACE_MS ma stessa semantica
753
753
  // del commit live che seguirà.
754
754
  graceDeadline: now() + PROOF_TTL_MS, lastCommit: null };
@@ -784,12 +784,12 @@ function handleChallengeProof(cellId, entry, socket, msg) {
784
784
  return { stablePath: entry.stablePath, launchEpoch };
785
785
  }
786
786
 
787
- // Recovery di produzione (R3.3.1 fail-closed): al boot del server la map e'
788
- // vuota e nessun lease sopravvive. Ricarichiamo {launchEpoch, graceDeadline}
789
- // persistiti PER CELLA e RIAPRIAMO l'endpoint stabile per ogni cella nota,
790
- // cosicche' un supervisore vivo che reconnecta dopo il restart trovi
791
- // l'endpoint. La verifica del proof non richiede stato ricostruito: la chiave
792
- // verifier e' persistita per-installazione.
787
+ // Production recovery (fail-closed): at server boot the map is empty
788
+ // and no lease survives. We reload the per-cell persisted
789
+ // {launchEpoch, graceDeadline} and REOPEN the stable endpoint for every
790
+ // known cell, so that a live supervisor reconnecting after the restart
791
+ // finds the endpoint. The proof check needs no rebuilt state: the verifier
792
+ // key is persisted per-installation.
793
793
  async function boot() {
794
794
  loadPersisted();
795
795
  for (const [cellId, entry] of cells) {
@@ -802,17 +802,17 @@ function handleChallengeProof(cellId, entry, socket, msg) {
802
802
  }
803
803
  }
804
804
 
805
- // Connessione broker one-shot iniziale (R3.1.1): autenticata dal nonce del
806
- // broker; qui associiamo la connessione al lease (primo contatto).
807
- // 2b: attachInitial NON scrive nulla sul canale. Durante la consegna del
808
- // payload il canale appartiene al protocollo del broker (frame length-
809
- // prefixed u32): una riga JSON scritta prima del payload corromperebbe la
810
- // lettura di receivePayload e il child non nascerebbe — misurato dal gate
811
- // (da revisione interna, esito positivo). Il proof supervisore arriva con
812
- // l'ACK del primo refresh, che il lease-client invia IMMEDIATAMENTE
813
- // all'avvio: la finestra attach->primo-ack senza proof detenuto e' fail-closed
814
- // (reconnect negato -> grace -> onLost), coerente col modello per cui nessun
815
- // commit persistito = nessuna recovery promessa.
805
+ // Initial one-shot broker connection: authenticated by the broker nonce;
806
+ // here we bind the connection to the lease (first contact).
807
+ // attachInitial writes nothing on the wire. While the payload is delivered
808
+ // the channel belongs to the broker protocol (length-prefixed u32 frames):
809
+ // a JSON line written before the payload would corrupt the receivePayload
810
+ // read and the child would never be born — measured by the gate
811
+ // (internal review, positive result). The supervisor proof arrives with the
812
+ // ACK of the first refresh, which the lease-client sends IMMEDIATELY at
813
+ // startup: the attach->first-ack window without a held proof is fail-closed
814
+ // (reconnect refused -> grace -> onLost), consistent with the model that no
815
+ // persisted commit means no promised recovery.
816
816
  function attachInitial(cellId, socket, { generation = 0 } = {}) {
817
817
  const entry = cells.get(cellId);
818
818
  if (!entry) return false;
@@ -821,11 +821,11 @@ function handleChallengeProof(cellId, entry, socket, msg) {
821
821
  return bindLiveSocket(cellId, entry, socket, { lease });
822
822
  }
823
823
 
824
- // Al boot del server (R3.3.1 fail-closed): la map e' vuota. Ricarichiamo solo
825
- // {launchEpoch, graceDeadline} persistiti PER CELLA (D1), per riconoscere le
826
- // identity (il proof le porta firmate) e per rifiutare reconnect oltre la
827
- // grace (R3.3.5). Nessun lease/eligibilita' sopravvive.
828
- // E3: un file malformato/corrotto salta la PROPRIA cella (logged); le altre
824
+ // At server boot (fail-closed): the map is empty. We reload only the
825
+ // {launchEpoch, graceDeadline} persisted PER CELL, to recognize the
826
+ // identities (the proof carries them signed) and to reject reconnects past
827
+ // the grace. No lease/eligibility survives.
828
+ // Un file malformato/corrotto salta la PROPRIA cella (logged); le altre
829
829
  // caricano. Con lo store unico di 2a un parse error buttava tutte.
830
830
  function loadPersisted() {
831
831
  let files;
@@ -857,7 +857,7 @@ function handleChallengeProof(cellId, entry, socket, msg) {
857
857
  // validarli anche quando la cella non sia ancora passata di nuovo per track().
858
858
  cells.set(cellId, {
859
859
  launchEpoch: info.launchEpoch,
860
- // IC8.5 (rev29): graceDeadline assente/illeggibile/non-intero = bound trattato come scaduto (0 -> deny sempre).
860
+ // A missing/unreadable/non-integer graceDeadline assente/illeggibile/non-intero = bound trattato come scaduto (0 -> deny sempre).
861
861
  // da revisione: forma <> semantica — un bound oltre now()+2*GRACE_MS non e' producibile
862
862
  // da questa fetta: trattato come scaduto (0). Tolleranza 2*GRACE_MS per un
863
863
  // restart durante la grace. Fail-closed: bound assurdo = scaduto.
@@ -868,7 +868,7 @@ function handleChallengeProof(cellId, entry, socket, msg) {
868
868
  }
869
869
  }
870
870
 
871
- // --- superficie child (B5): register / refresh / recovery -------------------
871
+ // -- superficie child: register / refresh / recovery -------------------
872
872
  //
873
873
  // Tre metodi DISTINTI perche' i loro valori di ritorno non possono mentire
874
874
  // l'uno con l'altro: register puo' rispondere 'pending' (la cella non e'
@@ -877,13 +877,13 @@ function handleChallengeProof(cellId, entry, socket, msg) {
877
877
  // un proof scaduto da poco. Un metodo unico costringerebbe 'refresh' a un
878
878
  // valore di ritorno che mente in uno dei due casi.
879
879
  //
880
- // B2: incarnationId e' PER-REGISTRATION, mai globale — il register di una
880
+ // IncarnationId e' PER-REGISTRATION, mai globale — il register di una
881
881
  // cella crea la propria incarnazione e non tocca le altre. Le registration
882
882
  // vivono in memoria e NON sopravvivono al restart del processo (coerente col
883
- // fail-closed R3.3.1 del lease): dopo un restart del server il child
884
- // re-registra (nuova incarnazione).
883
+ // fail-closed lease model): after a server restart the child
884
+ // re-registers (new incarnation).
885
885
  //
886
- // B3: l'unita' dell'attempt e' la PRESENTAZIONE di recovery, non la
886
+ // L'unita' dell'attempt e' la PRESENTAZIONE di recovery, non la
887
887
  // connessione — una connessione puo' portare piu' presentazioni, e contare
888
888
  // connessioni conterebbe la cosa sbagliata. Ogni presentazione (anche negata)
889
889
  // consuma un attempt della propria registration; oltre il cap la
@@ -891,7 +891,7 @@ function handleChallengeProof(cellId, entry, socket, msg) {
891
891
  const childRegs = new Map(); // cellId -> { incarnationId, createdAt, lastAt, recoveryAttempts }
892
892
  const RECOVERY_ATTEMPT_CAP = 8;
893
893
  // Finestre del child, derivate dalle misure del lease:
894
- // - un proof vive PROOF_TTL_MS (B8: 60s) dall'ultimo refresh;
894
+ // Un proof vive PROOF_TTL_MS (60s) dall'ultimo refresh;
895
895
  // - il recovery accetta un proof scaduto da meno di una grace (L.GRACE_MS):
896
896
  // il detentore ha saltato i refresh, non e' stato sostituito;
897
897
  // - quindi la registration e' viva fino a lastAt + PROOF_TTL_MS + GRACE_MS.
@@ -914,7 +914,7 @@ function handleChallengeProof(cellId, entry, socket, msg) {
914
914
  if (!validCellId(cellId)) return { status: 'denied', reason: 'cellId' };
915
915
  if (!cells.has(cellId)) {
916
916
  // La cella non e' (ancora) tracciata dal lease del supervisore: il join e'
917
- // pendente. Solo register puo' rispondere cosi' (B5).
917
+ // Pendente. Solo register puo' rispondere cosi'.
918
918
  return { status: 'pending', retryAfterMs: L.REFRESH_MS };
919
919
  }
920
920
  const launchSubject = cells.get(cellId) && cells.get(cellId).subject;
@@ -982,7 +982,7 @@ function handleChallengeProof(cellId, entry, socket, msg) {
982
982
  function childRecovery(cellId, proof) {
983
983
  const reg = childRegs.get(cellId);
984
984
  if (!reg) return { status: 'no-registration' };
985
- // B3: la presentazione conta PRIMA dell'esito — anche un tentativo negato
985
+ // La presentazione conta PRIMA dell'esito — anche un tentativo negato
986
986
  // consuma un attempt della registration.
987
987
  reg.recoveryAttempts += 1;
988
988
  if (reg.recoveryAttempts > RECOVERY_ATTEMPT_CAP) {
@@ -2,31 +2,31 @@
2
2
 
3
3
  // Pure state transitions for the supervisor lease of a Live host cell.
4
4
  //
5
- // Contratto rev12 R3 + rev13 S3 + rev22. Il canale lease e' la connessione UDS
5
+ // Contratto rev12 + rev13 + rev22. Il canale lease e' la connessione UDS
6
6
  // accettata col nonce one-shot del launch-broker che RESTA APERTA dopo il frame
7
- // payload (R3.1.1); dopo un EOF il supervisore si riconnette a un endpoint
8
- // stabile (R3.3.2). Qui vivono SOLO le transizioni di stato del lease: i side
9
- // effect (socket, timer, EOF handling) stanno nel LeaseManager e nei caller.
7
+ // payload; after an EOF the supervisor reconnects to a stable endpoint.
8
+ // Here live ONLY the lease state transitions: the side
9
+ // effects (socket, timer, EOF handling) live in the LeaseManager and the callers.
10
10
  // Modello speculare a lib/nodes/reverse-rotation.js (clone + arg validation),
11
11
  // cosi' i casi crash/replay sono espliciti e testabili senza socket.
12
12
  //
13
13
  // Invarianti normativi ribaditi nel codice:
14
- // - EOF arma UNA sola transizione monotona Live -> Grace (R3.1.3); la deadline
15
- // e' non estendibile: un secondo armGrace su un lease in Grace e' no-op.
14
+ // - EOF arms a SINGLE monotonic Live -> Grace transition; the deadline
15
+ // is not extendable: a second armGrace on a Grace lease is a no-op.
16
16
  // - Nessuna operazione riporta in Live un lease gia' in Grace: refresh e armGrace
17
17
  // non cambiano lo stato di un lease in Grace. Solo reattach crea un lease NUOVO.
18
- // - reattach (R3.3.4) produce un lease con leaseId NUOVO e STESSA identita'
19
- // cellId+launchEpoch (la generation puo' avanzare). Non e' la resurrezione del
20
- // lease precedente: il vecchio lease resta nella sua grace, la monotonia
21
- // per-lease resta intatta e la vecchia deadline diventa irrilevante.
22
- // - Oltre la grace il reconnect e' rifiutato (R3.3.5): reattach di un lease
23
- // scaduto restituisce null.
18
+ // - reattach produces a lease with a NEW leaseId and the SAME identity
19
+ // cellId+launchEpoch (the generation may advance). It is not the resurrection
20
+ // of the previous lease: the old lease stays in its grace, the per-lease
21
+ // monotonicity stays intact and the old deadline becomes irrelevant.
22
+ // - Past the grace the reconnect is refused: reattach of an expired
23
+ // lease returns null.
24
24
 
25
25
  const crypto = require('node:crypto');
26
26
 
27
- const GRACE_MS = 60_000; // R3.2: grace 60s dall'EOF, non estendibile
28
- const REFRESH_MS = 20_000; // R3.2: cadenza refresh 20s (heartbeat lato supervisore)
29
- const RECONNECT_CADENCE_MS = 20_000; // rev13 S3.3: >=2 tentativi strettamente dentro la grace
27
+ const GRACE_MS = 60_000; // grace: 60s from EOF, not extendable
28
+ const REFRESH_MS = 20_000; // refresh cadence 20s (supervisor-side heartbeat)
29
+ const RECONNECT_CADENCE_MS = 20_000; // >=2 attempts strictly inside the grace window
30
30
 
31
31
  function isInt(v) { return Number.isSafeInteger(v); }
32
32
  function validId(v, max = 128) { return typeof v === 'string' && v.length > 0 && v.length <= max; }
@@ -49,7 +49,7 @@ function openLease({ cellId, launchEpoch, generation, leaseId, now }) {
49
49
  }
50
50
 
51
51
  // EOF arma UNA sola transizione monotona Live -> Grace. Su un lease gia' in Grace
52
- // e' no-op (ritorna lo stesso lease): la deadline NON si estende (R3.1.3).
52
+ // is a no-op (returns the same lease): the deadline is NOT extendable.
53
53
  function armGrace(lease, { now } = {}) {
54
54
  if (!lease || !isInt(now)) return null;
55
55
  if (lease.state === 'grace') return lease;
@@ -61,16 +61,16 @@ function armGrace(lease, { now } = {}) {
61
61
  }
62
62
 
63
63
  // Refresh: heartbeat lato supervisore (cad 20s). Aggiorna lastRefreshedAt. NON
64
- // cambia state e NON riporta in Live un lease in Grace (R3.1.3). Hook per la
65
- // rotazione del proof HMAC (fetta 2b); in 2a e' solo liveness applicativa + ack.
64
+ // never changes state and never brings a Grace lease back to Live. Hook for the
65
+ // HMAC proof rotation; today it is application liveness + ack only.
66
66
  function refresh(lease, { now } = {}) {
67
67
  if (!lease || !isInt(now)) return null;
68
68
  return { ...lease, lastRefreshedAt: now };
69
69
  }
70
70
 
71
- // Reconnect riuscito (R3.3.4): lease NUOVO (leaseId nuovo), STESSA identita'
72
- // cellId+launchEpoch (la generation puo' avanzare), state Live. R3.3.5: oltre la
73
- // grace il reconnect e' rifiutato -> null. Il vecchio lease resta nella sua grace.
71
+ // Successful reconnect: NEW lease (new leaseId), SAME identity
72
+ // cellId+launchEpoch (the generation may advance), state Live. Past the
73
+ // grace the reconnect is refused -> null. The old lease stays in its grace.
74
74
  function reattach(lease, { leaseId, generation, now } = {}) {
75
75
  if (!lease || !validLeaseId(leaseId) || !isInt(generation) || generation < 0 || !isInt(now)) return null;
76
76
  if (lease.state === 'grace' && now >= lease.graceDeadline) return null;