@mmmbuto/nexuscrew 0.9.28 → 0.9.30
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 +16 -0
- package/docs/LIVE_PROMPT.md +1 -1
- package/docs/NOTIFICATIONS.md +37 -0
- package/docs/OPERATIONS.md +20 -0
- package/frontend/dist/assets/index-BbcjoZS0.js +167 -0
- package/frontend/dist/assets/index-DqCfZFGi.css +32 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/sw.js +91 -10
- package/frontend/dist/version.json +1 -1
- package/lib/audio/groups.js +1 -1
- package/lib/auth/token.js +4 -4
- package/lib/cli/commands.js +70 -15
- package/lib/cli/doctor.js +5 -5
- package/lib/cli/fleet-service.js +7 -7
- package/lib/cli/init.js +55 -31
- package/lib/cli/pidfile.js +1 -1
- package/lib/cli/service.js +124 -8
- package/lib/cli/stable-alias.js +4 -4
- package/lib/cli/url.js +1 -1
- package/lib/config.js +36 -6
- package/lib/files/routes.js +1 -1
- package/lib/fleet/boot.js +2 -2
- package/lib/fleet/builtin.js +15 -15
- package/lib/fleet/causes.js +1 -1
- package/lib/fleet/cell-exec.js +23 -23
- package/lib/fleet/cell-hold.js +6 -6
- package/lib/fleet/cell-lease-server.js +106 -106
- package/lib/fleet/cell-lease.js +21 -21
- package/lib/fleet/definitions.js +10 -10
- package/lib/fleet/identity-authority.js +1 -1
- package/lib/fleet/launch-broker.js +13 -13
- package/lib/fleet/launch.js +15 -15
- package/lib/fleet/lease-client.js +23 -23
- package/lib/fleet/lease-routes.js +3 -3
- package/lib/fleet/lease-verifier.js +22 -22
- package/lib/fleet/managed.js +35 -35
- package/lib/fleet/prompt-delivery.js +13 -13
- package/lib/fleet/routes.js +15 -10
- package/lib/fleet/runtime.js +24 -24
- package/lib/live-host/bridge.js +53 -43
- package/lib/live-host/federation.js +97 -0
- package/lib/live-host/routes.js +166 -8
- package/lib/live-host/store.js +44 -6
- package/lib/mcp/server.js +14 -14
- package/lib/mcp/tools.js +5 -5
- package/lib/nodes/access-presets.js +258 -0
- package/lib/nodes/commands.js +198 -4
- package/lib/nodes/store.js +152 -9
- package/lib/nodes/tunnel.js +7 -7
- package/lib/notify/ask-answer-service.js +184 -0
- package/lib/notify/ask-receipts.js +197 -0
- package/lib/notify/ask-relay.js +152 -0
- package/lib/notify/asks.js +28 -3
- package/lib/notify/event-feed-acl.js +95 -0
- package/lib/notify/event-feed-asks-routes.js +144 -0
- package/lib/notify/event-feed-client.js +434 -0
- package/lib/notify/event-feed-history.js +115 -0
- package/lib/notify/event-feed-producers.js +175 -0
- package/lib/notify/event-feed-routes.js +254 -0
- package/lib/notify/event-feed.js +328 -0
- package/lib/notify/events.js +1 -1
- package/lib/notify/notifier.js +3 -3
- package/lib/notify/push-relay.js +240 -0
- package/lib/notify/push.js +8 -8
- package/lib/notify/routes.js +62 -23
- package/lib/proxy/federation.js +132 -12
- package/lib/proxy/node-proxy.js +13 -13
- package/lib/proxy/panel-auth.js +4 -4
- package/lib/proxy/panel-proxy.js +2 -2
- package/lib/proxy/resource-acl.js +145 -0
- package/lib/server.js +396 -40
- package/lib/settings/pairing-coordinator.js +1 -1
- package/lib/settings/routes.js +45 -32
- package/lib/update/manager.js +1 -1
- package/lib/update/runner.js +1 -1
- package/lib/voice/transcribe.js +1 -1
- package/lib/ws/bridge.js +3 -3
- package/package.json +1 -1
- package/skills/live/SKILL.md +12 -0
- package/frontend/dist/assets/index-CWra6B79.js +0 -167
- package/frontend/dist/assets/index-D6CIfCjv.css +0 -32
|
@@ -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
|
-
//
|
|
8
|
-
//
|
|
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
|
-
// (
|
|
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
|
-
// (
|
|
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
|
-
//
|
|
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
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
83
|
-
//
|
|
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
|
|
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 (
|
|
95
|
+
// Le chiavi vive per la verifica (-ready, oggi una: sospesa).
|
|
96
96
|
const liveKeys = () => [ensureVerifier()];
|
|
97
97
|
|
|
98
|
-
// Registro jti consumati
|
|
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
|
|
114
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
177
|
+
// The persisted graceDeadline SEMPRE valorizzato come intero valido (mai
|
|
178
178
|
// null) — bound durevole per rifiutare reconnect oltre la grace post-restart
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
201
|
-
//
|
|
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); //
|
|
206
|
+
const D = Math.max(current, proposed); // Mai all'indietro
|
|
207
207
|
if (D <= t) {
|
|
208
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
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
|
-
//
|
|
318
|
-
//
|
|
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
|
-
//
|
|
322
|
-
// post-restart (
|
|
323
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
611
|
-
//
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
// retry/reattach)
|
|
615
|
-
//
|
|
616
|
-
// client
|
|
617
|
-
//
|
|
618
|
-
//
|
|
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
|
-
//
|
|
626
|
-
//
|
|
627
|
-
//
|
|
628
|
-
//
|
|
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
|
-
//
|
|
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})`); //
|
|
643
|
-
//
|
|
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
|
-
//
|
|
657
|
-
//
|
|
658
|
-
// stableServer
|
|
659
|
-
// boot() (
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
788
|
-
//
|
|
789
|
-
//
|
|
790
|
-
//
|
|
791
|
-
//
|
|
792
|
-
//
|
|
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
|
-
//
|
|
806
|
-
//
|
|
807
|
-
//
|
|
808
|
-
//
|
|
809
|
-
//
|
|
810
|
-
//
|
|
811
|
-
// (
|
|
812
|
-
//
|
|
813
|
-
//
|
|
814
|
-
// (reconnect
|
|
815
|
-
// commit
|
|
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
|
-
//
|
|
825
|
-
// {launchEpoch, graceDeadline}
|
|
826
|
-
//
|
|
827
|
-
// grace
|
|
828
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
884
|
-
// re-
|
|
883
|
+
// fail-closed lease model): after a server restart the child
|
|
884
|
+
// re-registers (new incarnation).
|
|
885
885
|
//
|
|
886
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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) {
|
package/lib/fleet/cell-lease.js
CHANGED
|
@@ -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
|
|
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
|
|
8
|
-
//
|
|
9
|
-
//
|
|
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
|
|
15
|
-
//
|
|
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
|
|
19
|
-
// cellId+launchEpoch (
|
|
20
|
-
// lease
|
|
21
|
-
//
|
|
22
|
-
// -
|
|
23
|
-
//
|
|
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; //
|
|
28
|
-
const REFRESH_MS = 20_000; //
|
|
29
|
-
const RECONNECT_CADENCE_MS = 20_000; //
|
|
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
|
-
//
|
|
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
|
-
//
|
|
65
|
-
//
|
|
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
|
-
//
|
|
72
|
-
// cellId+launchEpoch (
|
|
73
|
-
// 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;
|