@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.
Files changed (81) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/docs/LIVE_PROMPT.md +1 -1
  3. package/docs/NOTIFICATIONS.md +37 -0
  4. package/docs/OPERATIONS.md +20 -0
  5. package/frontend/dist/assets/index-BbcjoZS0.js +167 -0
  6. package/frontend/dist/assets/index-DqCfZFGi.css +32 -0
  7. package/frontend/dist/index.html +2 -2
  8. package/frontend/dist/sw.js +91 -10
  9. package/frontend/dist/version.json +1 -1
  10. package/lib/audio/groups.js +1 -1
  11. package/lib/auth/token.js +4 -4
  12. package/lib/cli/commands.js +70 -15
  13. package/lib/cli/doctor.js +5 -5
  14. package/lib/cli/fleet-service.js +7 -7
  15. package/lib/cli/init.js +55 -31
  16. package/lib/cli/pidfile.js +1 -1
  17. package/lib/cli/service.js +124 -8
  18. package/lib/cli/stable-alias.js +4 -4
  19. package/lib/cli/url.js +1 -1
  20. package/lib/config.js +36 -6
  21. package/lib/files/routes.js +1 -1
  22. package/lib/fleet/boot.js +2 -2
  23. package/lib/fleet/builtin.js +15 -15
  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 +10 -10
  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 +35 -35
  37. package/lib/fleet/prompt-delivery.js +13 -13
  38. package/lib/fleet/routes.js +15 -10
  39. package/lib/fleet/runtime.js +24 -24
  40. package/lib/live-host/bridge.js +53 -43
  41. package/lib/live-host/federation.js +97 -0
  42. package/lib/live-host/routes.js +166 -8
  43. package/lib/live-host/store.js +44 -6
  44. package/lib/mcp/server.js +14 -14
  45. package/lib/mcp/tools.js +5 -5
  46. package/lib/nodes/access-presets.js +258 -0
  47. package/lib/nodes/commands.js +198 -4
  48. package/lib/nodes/store.js +152 -9
  49. package/lib/nodes/tunnel.js +7 -7
  50. package/lib/notify/ask-answer-service.js +184 -0
  51. package/lib/notify/ask-receipts.js +197 -0
  52. package/lib/notify/ask-relay.js +152 -0
  53. package/lib/notify/asks.js +28 -3
  54. package/lib/notify/event-feed-acl.js +95 -0
  55. package/lib/notify/event-feed-asks-routes.js +144 -0
  56. package/lib/notify/event-feed-client.js +434 -0
  57. package/lib/notify/event-feed-history.js +115 -0
  58. package/lib/notify/event-feed-producers.js +175 -0
  59. package/lib/notify/event-feed-routes.js +254 -0
  60. package/lib/notify/event-feed.js +328 -0
  61. package/lib/notify/events.js +1 -1
  62. package/lib/notify/notifier.js +3 -3
  63. package/lib/notify/push-relay.js +240 -0
  64. package/lib/notify/push.js +8 -8
  65. package/lib/notify/routes.js +62 -23
  66. package/lib/proxy/federation.js +132 -12
  67. package/lib/proxy/node-proxy.js +13 -13
  68. package/lib/proxy/panel-auth.js +4 -4
  69. package/lib/proxy/panel-proxy.js +2 -2
  70. package/lib/proxy/resource-acl.js +145 -0
  71. package/lib/server.js +396 -40
  72. package/lib/settings/pairing-coordinator.js +1 -1
  73. package/lib/settings/routes.js +45 -32
  74. package/lib/update/manager.js +1 -1
  75. package/lib/update/runner.js +1 -1
  76. package/lib/voice/transcribe.js +1 -1
  77. package/lib/ws/bridge.js +3 -3
  78. package/package.json +1 -1
  79. package/skills/live/SKILL.md +12 -0
  80. package/frontend/dist/assets/index-CWra6B79.js +0 -167
  81. package/frontend/dist/assets/index-D6CIfCjv.css +0 -32
@@ -1,13 +1,13 @@
1
1
  'use strict';
2
- // B4.1 — Definizioni fleet editabili (~/.nexuscrew/fleet.json).
2
+ // Editable fleet definitions (~/.nexuscrew/fleet.json).
3
3
  // Modulo PURO: nessun side-effect all'import. Tutto l'I/O vive in
4
4
  // loadDefinitions/atomicWrite; parseDefinitions/validateCommandTrust non
5
5
  // toccano il filesystem se non per le stat di trust (sincrone, come binTrusted).
6
6
  //
7
7
  // Principio: fail-closed. Qualunque dato malformato -> null, MAI throw non
8
- // gestito. Le definizioni contengono comandi arbitrari (design §6), quindi la
9
- // validazione e' STRICT (garbage -> errore, non guess). Stesso confinamento di
10
- // lib/fs/routes.js e lib/tmux/lifecycle.js.
8
+ // handled. The definitions carry arbitrary commands, so validation is
9
+ // STRICT (garbage -> error, no guessing). Same confinement as
10
+ // lib/fs/routes.js and lib/tmux/lifecycle.js.
11
11
  const fs = require('node:fs');
12
12
  const path = require('node:path');
13
13
  const crypto = require('node:crypto');
@@ -119,7 +119,7 @@ function isTmuxSafeName(name) {
119
119
  // Deriva il nome sessione tmux CANONICO di una cella. Puro, iniettivo, reversibile.
120
120
  // - ID senza punto: storico `cloud-<id>` (le sessioni esistenti non vengono
121
121
  // rinominate; il namespace e' tmux-safe perche' l'id stesso non ha punto).
122
- // - ID con punto: mapping v2 dot-free (design §3.1.5):
122
+ // - Dotted IDs: dot-free v2 mapping:
123
123
  // raw = base64url(UTF-8 id), senza padding '='
124
124
  // n = lunghezza di raw su due cifre
125
125
  // padded = raw right-padded con '-' fino a 43 caratteri
@@ -469,7 +469,7 @@ function parseCell(c, engineIds, engineMap = new Map(), {
469
469
  // cwd (obbligatorio; la risoluzione/confinamento avviene via resolveCwd a runtime)
470
470
  if (typeof c.cwd !== 'string' || !c.cwd || c.cwd.length > MAX_CWD_LEN) return null;
471
471
 
472
- // cwdRel (opzionale canonico, design §4.3): forma portatile home-relative.
472
+ // cwdRel (optional, canonical): portable home-relative form.
473
473
  // Qui si valida solo il FORMATO (stringa canonica): la coerenza cwd<->cwdRel
474
474
  // e' un invariante di SCRITTURA (define/edit/restore), non di lettura — così
475
475
  // le definizioni legacy (solo cwd) e quelle nuove (cwd+cwdRel) caricano senza
@@ -907,9 +907,9 @@ function validateCommandTrust(command) {
907
907
  if (!st.isFile()) return { ok: false, reason: 'non e\' un file regolare (symlink o speciale)' }; // lstat: symlink -> isFile()=false
908
908
  if (!(st.mode & 0o100)) return { ok: false, reason: 'non eseguibile dall\'owner' };
909
909
  if (st.mode & 0o002) return { ok: false, reason: 'world-writable' };
910
- // Owner check (design §9a, da revisione): il command deve appartenere
911
- // all'utente del service o a root — un owner terzo potrebbe sostituire
912
- // l'eseguibile mantenendo il path "trusted".
910
+ // Owner check: the command must belong to the service user or root —
911
+ // a third-party owner could replace the executable while keeping the
912
+ // path "trusted".
913
913
  if (typeof process.getuid === 'function') {
914
914
  const uid = process.getuid();
915
915
  if (st.uid !== uid && st.uid !== 0) return { ok: false, reason: 'owner non fidato (ne\' utente del service ne\' root)' };
@@ -948,7 +948,7 @@ function resolveCwd(cwd, home, out) {
948
948
  }
949
949
 
950
950
  // ---------------------------------------------------------------------------
951
- // cwdRel — cwd home-relative PORTATILE (design §4.3 / backup v3).
951
+ // cwdRel — portable home-relative cwd representation.
952
952
  // Rappresentazione canonica di una cwd come percorso relativo alla home del
953
953
  // device target: '' == la home stessa; 'personal' == <home>/personal.
954
954
  // Helper PURI (nessun fs): la normalizzazione e' string-only e fail-closed.
@@ -294,7 +294,7 @@ function createIdentityAuthority({
294
294
  return { ok: false, reason: 'challenge' };
295
295
  }
296
296
  const currentTick = Number(now());
297
- // R2: il TTL della challenge e' SERVER-OWNED (15s dall'orologio
297
+ // Il TTL della challenge e' SERVER-OWNED (15s dall'orologio
298
298
  // dell'authority), non del chiamante: oltre il cap viene capito, e una
299
299
  // challenge con issuedAt nel futuro e' rifiutata (documentato in referto).
300
300
  if (Number.isSafeInteger(challenge.issuedAt) && challenge.issuedAt > currentTick) {
@@ -77,11 +77,11 @@ function createLaunchBroker(cfg = {}) {
77
77
  let closed = false;
78
78
  const pending = new Map();
79
79
  const ttlMs = Math.max(1000, Number(cfg.launchTokenTtlMs) || 15000);
80
- // R3.1.1 (opt-in): se cfg.onLease e' registrato e il payload porta `lease`, la
81
- // connessione accettata col nonce one-shot resta APERTA dopo il frame payload e
82
- // viene consegnata a onLease(socket, lease) per divenire il canale lease. Senza
83
- // onLease o senza lease il comportamento e' invariato (socket.end, one-shot):
84
- // le celle non-ospite non sono toccate.
80
+ // Opt-in: if cfg.onLease is registered and the payload carries `lease`, the
81
+ // connection accepted with the one-shot nonce stays OPEN after the payload
82
+ // frame and is handed to onLease(socket, lease) to become the lease channel.
83
+ // Without onLease or without a lease the behavior is unchanged (socket.end,
84
+ // one-shot): non-host cells are untouched.
85
85
  const onLease = typeof cfg.onLease === 'function' ? cfg.onLease : null;
86
86
  const identityMode = cfg.identityMode || 'legacy';
87
87
  const identityAuthority = cfg.identityAuthority || null;
@@ -127,12 +127,12 @@ function createLaunchBroker(cfg = {}) {
127
127
  // never claim the same payload, even while the first socket drains.
128
128
  pending.delete(nonce); clearTimeout(entry.timer);
129
129
  if (entry.lease && onLease) {
130
- // R3.1.1: la stessa connessione resta APERTA dopo il frame payload.
131
- // Disarmiamo il timeout one-shot e rimuoviamo il data handler del nonce:
132
- // la socket arriva pulita a onLease, che la dedica al canale lease. Cosi'
133
- // un refresh successivo non riparla sul nonce esaurito (il reparse della
134
- // riga nonce, con pending vuota, distruggeva la connessione) e non scatta
135
- // piu' il timeout idle da 3s pensato per il one-shot.
130
+ // The same connection stays OPEN after the payload frame.
131
+ // We disarm the one-shot timeout and remove the nonce data handler:
132
+ // the socket reaches onLease clean, which dedicates it to the lease
133
+ // channel: a later refresh never talks to the spent nonce (the reparse
134
+ // of the nonce line, with an empty pending map, used to destroy the
135
+ // connection) and the 3s idle timeout meant for the one-shot is gone.
136
136
  socket.setTimeout(0);
137
137
  socket.removeAllListeners('data');
138
138
  // da revisione interna: la lease PRIMA del payload. onLease
@@ -267,8 +267,8 @@ function createLaunchBroker(cfg = {}) {
267
267
  try { if (socketPath) fs.unlinkSync(socketPath); } catch (_) {}
268
268
  }
269
269
 
270
- // Revoca esplicita di un nonce pendente (design §3.3): se il respawn-pane
271
- // fallisce dopo issue(), il runtime consuma/revoca il ticket subito invece di
270
+ // Explicit revocation of a pending nonce: if the respawn-pane fails after
271
+ // issue(), the runtime consumes/revokes the ticket immediately instead of
272
272
  // attenderne il TTL. expire() e' gia' no-op su nonce mancante/scaduto.
273
273
  return { issue, close, revoke: expire, pendingCount: () => pending.size };
274
274
  }
@@ -5,7 +5,7 @@
5
5
  // del fleet. createBuiltinRuntime() in runtime.js ne fa uso; builtin.js e'
6
6
  // ora un facade che re-esporta questi simboli per i test.
7
7
  //
8
- // Sicurezza (design §9a/§9e/§9h) — invariata rispetto a builtin.js:
8
+ // Security model — unchanged versus builtin.js:
9
9
  // - command/args/env NON passano per una shell: execFile + argv diretto
10
10
  // (tmux fa exec del comando, NON sh -c — verificato: ';','|','$' passano
11
11
  // verbatim). Nessun valore passa in argv, `tmux -e`, file temporanei o
@@ -14,8 +14,8 @@
14
14
  // possono toccare PATH/loader-key (parseDefinitions le rifiuta gia' in env).
15
15
  // - promptMode 'send-keys' inietta via `tmux load-buffer` + `paste-buffer -p`
16
16
  // (bracketed paste), NON send-keys grezzo; se il command e' gia' uscito
17
- // (sessione morta) NON digita (§9e).
18
- // - redactSecrets/sanitizeEarlyDiagnostic (§9h): stderr/stdout dei comandi
17
+ // (dead session) it does not type.
18
+ // - redactSecrets/sanitizeEarlyDiagnostic: stderr/stdout of failed tmux
19
19
  // tmux falliti NON devono mai ecoare i segreti delle definizioni.
20
20
  const fs = require('node:fs');
21
21
  const os = require('node:os');
@@ -26,7 +26,7 @@ const { minimalRuntimeEnv } = require('../runtime/env.js');
26
26
  const { codeOf, phaseOf } = require('./causes.js');
27
27
  const { isTmuxSafeName, tmuxSessionForCell } = require('./definitions.js');
28
28
 
29
- // Env minimale controllato dal service (design §9a). Allowlist DURA: le definizioni
29
+ // Minimal service-controlled env. HARD allowlist: the definitions
30
30
  // non possono toccare PATH/loader-key (parseDefinitions le rifiuta gia' in env);
31
31
  // qui NON passiamo MAI l'env del processo per intero. engine.env viene consegnato
32
32
  // direttamente al processo figlio dal broker, senza entrare nello stato tmux.
@@ -52,7 +52,7 @@ function tmuxInventoryEnv() {
52
52
  }
53
53
 
54
54
  // httpError(status, msg, data?, cause?) — structured HTTP error. `data` carries
55
- // arbitrary API detail for the response body; `cause` (T4) is the OPTIONAL
55
+ // Arbitrary API detail for the response body; `cause` is the OPTIONAL
56
56
  // bounded failure triple {phase, code} of the up() boundary that failed. The
57
57
  // cause is coerced through the closed enum in causes.js (anything not
58
58
  // allowlisted degrades to UNKNOWN) and attached as e.fleetCode / e.fleetPhase,
@@ -70,12 +70,12 @@ function httpError(status, msg, data = null, cause = null) {
70
70
  return e;
71
71
  }
72
72
 
73
- // Marcatore di redazione (design §9h): stderr/stdout dei comandi tmux falliti
73
+ // Redaction marker: stderr/stdout of failed tmux commands
74
74
  // NON devono mai ecoare i segreti delle definizioni.
75
75
  const REDACTED = '‹redacted›';
76
76
 
77
77
  // redactSecrets(text, engine, cell) -> string con ogni occorrenza dei segreti
78
- // delle definizioni sostituita da '‹redacted›'. Segreti coperti (§9h):
78
+ // of the definitions replaced by '‹redacted›'. Covered secrets:
79
79
  // - valori di engine.env (le CHIAVI restano, i VALUES vengono redatti)
80
80
  // - testo del prompt della cella (cell.prompt)
81
81
  // - testo del prompt dell'engine (engine.prompt) se presente
@@ -156,7 +156,7 @@ function boundedTmuxFailure(result) {
156
156
  .replace(/[\x00-\x1f\x7f]+/g, ' ').trim().slice(0, 240) || 'errore tmux';
157
157
  }
158
158
 
159
- // Migrazione identita' tmux (design §3.2). Prima costruisce e valida TUTTO il
159
+ // tmux identity migration: the whole plan is built and validated first.
160
160
  // piano: nessun rename parte se una sessione legacy normalizzata e' rivendicata
161
161
  // da piu celle (es. a.b vs a_b), se legacy+target safe coesistono o se il target
162
162
  // non e' tmux-safe. Solo dopo il preflight rinomina via `$N`; qualunque errore e'
@@ -267,7 +267,7 @@ async function migrateLegacyTmuxSessions(tmuxBin, defs, { readonly = false } = {
267
267
  };
268
268
  }
269
269
 
270
- // Policy caratteri del prompt send-keys (§9e): ammette stampabili + \t \n \r;
270
+ // send-keys prompt character policy: printable + \t \n \r allowed;
271
271
  // rifiuta ESC(0x1b) e gli altri byte di controllo (niente marker bracketed-paste
272
272
  // iniettabili). parseDefinitions caps solo la lunghezza: questo e' defense-in-depth.
273
273
  function promptCharsOk(prompt) {
@@ -293,7 +293,7 @@ function composeClientInvocation(engine, cell) {
293
293
  if (val) args.push(engine.model.flag, val);
294
294
  }
295
295
  // prompt flag-mode: promptFlag + prompt cella, solo se c'e' un prompt effettivo.
296
- // SICUREZZA (design §9h): promptMode 'flag' mette il prompt in ARGV -> e' visibile
296
+ // SECURITY: promptMode 'flag' puts the prompt in ARGV -> it is visible
297
297
  // nella process list (ps) / argv della sessione, a differenza di 'send-keys' che lo
298
298
  // inietta DOPO via bracketed paste. Va quindi vincolato a prompt NON-segreti.
299
299
  if (engine.promptMode === 'flag' && cell.prompt) {
@@ -357,7 +357,7 @@ async function waitStablePane(tmuxBin, target, { env, readyMs }) {
357
357
  }
358
358
 
359
359
  // Risolve il pane id (%N) esatto della sessione, o verifica un %N gia' noto.
360
- // Contratto submitToSession (R5): output session_name + pane_dead + pane_id,
360
+ // Contratto submitToSession: output session_name + pane_dead + pane_id,
361
361
  // parsing ESATTO per campi (mai regex %N libera), session verificata uguale
362
362
  // a quella attesa prima di qualunque paste/Enter. Ritorna null se il pane non
363
363
  // e' risolvibile, morto, di un'altra sessione o diverso dal %N atteso.
@@ -403,10 +403,10 @@ async function securePaste(tmuxBin, session, text, { env, exec, tmpdir, target,
403
403
  const load = await run(tmuxBin, ['load-buffer', '-b', buffer, tmp], { env });
404
404
  if (load.err) return { ok: false, stage: 'load', reason: 'buffer non disponibile' };
405
405
  loaded = true;
406
- // R4: da qui in poi il paste e' stato TENTATO: qualunque throw/rejection
406
+ // Da qui in poi il paste e' stato TENTATO: qualunque throw/rejection
407
407
  // dell'executor e' post-paste (stage 'paste'), mai classificato come
408
408
  // pre-paste 'load' — il composer potrebbe contenere testo parziale o
409
- // completo e un retry duplicherebbe il prompt (G1).
409
+ // Completo e un retry duplicherebbe il prompt.
410
410
  try {
411
411
  const paste = await run(tmuxBin, ['paste-buffer', '-p', '-t', paneId, '-b', buffer], { env });
412
412
  if (paste.err) return { ok: false, stage: 'paste', reason: 'paste-buffer failed' };
@@ -423,9 +423,9 @@ async function securePaste(tmuxBin, session, text, { env, exec, tmpdir, target,
423
423
  }
424
424
 
425
425
  // Iniezione prompt send-keys via bracketed paste (contratto legacy per engine
426
- // custom promptMode 'send-keys': paste SENZA Enter, invariato — 0.8.47 G4).
426
+ // Custom promptMode 'send-keys': paste SENZA Enter, invariato — 0.8.47).
427
427
  // Readiness best-effort: se la sessione non e' viva quando paste-iamo (command
428
- // gia' uscito) NON digita (design §9e). Ritorna {injected, reason}.
428
+ // already exited) does not type. Returns {injected, reason}.
429
429
  // Trasporto backportato su securePaste: buffer random, temp wx/0600, pane %N.
430
430
  async function injectPrompt(tmuxBin, session, prompt, { env, readyMs = 400, target, engine, cell } = {}) {
431
431
  if (!promptCharsOk(prompt)) {
@@ -2,16 +2,16 @@
2
2
 
3
3
  // Lease-client lato SUPERVISORE (cell-exec.js).
4
4
  //
5
- // Mantiene la connessione lease iniziale (la stessa del broker one-shot che resta
6
- // APERTA dopo il payload, R3.1.1), invia refresh a cadenza 20s (R3.2 heartbeat),
7
- // e su EOF reconnecta all'endpoint stabile (R3.3.2).
5
+ // Keeps the initial lease connection (the same one-shot broker connection that
6
+ // stays OPEN after the payload), sends refreshes every 20s (heartbeat),
7
+ // and on EOF reconnects to the stable endpoint.
8
8
  //
9
- // Fetta 2b (contratto rev1, A2/B1): l'autenticazione del reconnect e' un proof
9
+ // Fetta 2b (contratto rev1, /): l'autenticazione del reconnect e' un proof
10
10
  // HMAC firmato dal server col verifier per-installazione — la capability statica
11
11
  // della 2a e' revocata. Il proof NON arriva nel payload: il server lo consegna
12
12
  // sul canale (frame lease all'attach, ack a ogni refresh) e il supervisore lo
13
- // detiene in memoria, presentandolo tale e quale al reconnect. R3.1.2 invariato:
14
- // niente di tutto questo transita mai nell'env del child.
13
+ // held in memory, presented as-is on reconnect. Unchanged:
14
+ // none of this ever transits in the child env.
15
15
  //
16
16
  // Side effect isolati e iniettabili (seams) per testabilita', come altrove.
17
17
 
@@ -33,7 +33,7 @@ function validDaemonChallenge(value) {
33
33
  const keys = Object.keys(value);
34
34
  if (keys.length !== IDENTITY_CHALLENGE_KEYS.length
35
35
  || IDENTITY_CHALLENGE_KEYS.some((key) => !Object.prototype.hasOwnProperty.call(value, key))) return false;
36
- // R4: tipi JSON originari, niente coercizioni (Number()/regex su non-stringhe).
36
+ // Tipi JSON originari, niente coercizioni (Number/regex su non-stringhe).
37
37
  const issuedAt = value.issuedAt;
38
38
  const expiresAt = value.expiresAt;
39
39
  return value.version === 1
@@ -90,13 +90,13 @@ function startLeaseClient(initialSocket, info, seams = {}) {
90
90
  let refreshTimer = null;
91
91
  let reconnectTimer = null;
92
92
  // 2b: ultimo proof consegnato dal server sul canale (attach/refresh). Vive
93
- // SOLO in memoria qui: e' effimero per costruzione (B8), non si persiste.
93
+ // SOLO in memoria qui: e' effimero per costruzione, non si persiste.
94
94
  let heldProof = null;
95
- // R3.2: bound di grace per i reconnect (eofAt + GRACE_MS). Oltre non si ritenta.
95
+ // Grace bound for reconnects (eofAt + GRACE_MS). Past it, no more attempts.
96
96
  let reconnectDeadline = null;
97
97
  const identityPending = new Map();
98
98
  const verifyPending = new Map();
99
- // R1: una sola transizione di generazione in volo (il supervisore e' sequenziale).
99
+ // Una sola transizione di generazione in volo (il supervisore e' sequenziale).
100
100
  let generationWaiter = null;
101
101
 
102
102
  function send(obj) {
@@ -150,8 +150,8 @@ function startLeaseClient(initialSocket, info, seams = {}) {
150
150
  if (current) { try { current.removeAllListeners('data'); current.removeAllListeners('close'); current.removeAllListeners('end'); } catch (_) {} }
151
151
  current = null;
152
152
  clearTimer(refreshTimer);
153
- // R3.2: la grace parte dall'EOF lato supervisore. I reconnect sono bounded da
154
- // eofAt + GRACE_MS: rev13 S3.3 garantisce >=2 tentativi STRETTAMENTE dentro.
153
+ // The grace starts from the supervisor-side EOF. Reconnects are bounded by
154
+ // eofAt + GRACE_MS: at least 2 attempts happen STRICTLY inside it.
155
155
  reconnectDeadline = now() + L.GRACE_MS;
156
156
  armReconnect(0); // primo tentativo subito, poi a cadenza RECONNECT_CADENCE_MS
157
157
  }
@@ -160,7 +160,7 @@ function startLeaseClient(initialSocket, info, seams = {}) {
160
160
 
161
161
  function armReconnect(delay) {
162
162
  clearTimer(reconnectTimer);
163
- // R3.2: oltre la grace non si ritenta (deny o server muto non vanno in loop).
163
+ // Past the grace no more attempts (deny or mute server must not loop).
164
164
  if (reconnectDeadline != null && now() >= reconnectDeadline) {
165
165
  // da revisione interna: la grace e' scaduta senza un reconnect
166
166
  // riuscito: la lease e' PERSA per questo supervisore. Prima si desisteva
@@ -182,8 +182,8 @@ function startLeaseClient(initialSocket, info, seams = {}) {
182
182
 
183
183
  function attemptReconnect() {
184
184
  let settled = false;
185
- // R3.3.4: generation puo' essere un getter (cell-exec la fa avanzare coi restart)
186
- // o un valore; al reconnect presentiamo sempre quella corrente.
185
+ // The generation may be a getter (cell-exec advances it with restarts)
186
+ // or a value; on reconnect we always present the current one.
187
187
  const curGeneration = typeof info.generation === 'function' ? info.generation() : (info.generation || 0);
188
188
  const sock = netImpl.createConnection(info.stablePath, () => {
189
189
  // 2b: presentiamo il proof detenuto. Senza proof (nessun ack ricevuto, o
@@ -192,9 +192,9 @@ function startLeaseClient(initialSocket, info, seams = {}) {
192
192
  const msg = { type: 'reconnect', generation: curGeneration, ...(heldProof ? { proof: heldProof } : {}) };
193
193
  try { sock.write(`${JSON.stringify(msg)}\n`); } catch (_) { try { sock.destroy(); } catch (e) {} }
194
194
  });
195
- // R3.2: per-attempt timeout. Se il server accetta la connessione ma non risponde
196
- // (socket appesa) forziamo la chiusura e ritentiamo: senza questo il client
197
- // restava con 1 solo tentativo e 0 timer successivi. Cadence come upper bound.
195
+ // Per-attempt timeout. If the server accepts the connection but does not
196
+ // answer (hanging socket) we force-close and retry: without this the client
197
+ // stayed with 1 attempt and 0 following timers. Cadence as the upper bound.
198
198
  const attemptTimer = setTimer(() => {
199
199
  if (settled) return;
200
200
  settled = true;
@@ -214,8 +214,8 @@ function startLeaseClient(initialSocket, info, seams = {}) {
214
214
  if (msg.type === 'lease') {
215
215
  settled = true;
216
216
  clearTimer(attemptTimer);
217
- // R3.3.4: reconnect riuscito, lease nuovo. Il proof consegnato col lease
218
- // nuovo diventa quello detenuto; riprende il refresh loop.
217
+ // Successful reconnect, new lease. The proof delivered with the new
218
+ // lease becomes the held one; the refresh loop resumes.
219
219
  clearTimer(reconnectTimer);
220
220
  if (msg.proof && typeof msg.proof === 'object') heldProof = msg.proof;
221
221
  current = sock;
@@ -224,8 +224,8 @@ function startLeaseClient(initialSocket, info, seams = {}) {
224
224
  } else if (msg.type === 'deny') {
225
225
  settled = true;
226
226
  clearTimer(attemptTimer);
227
- // R3.3.5: rifiutato (proof rifiutato, oltre grace o identity). Riprova a
228
- // cadenza: rev13 S3.3 garantisce >=2 tentativi strettamente dentro la grace.
227
+ // Refused (bad proof, past grace, or identity mismatch). Retry at
228
+ // cadence: at least 2 attempts happen strictly inside the grace window.
229
229
  try { sock.destroy(); } catch (_) {}
230
230
  armReconnect(L.RECONNECT_CADENCE_MS);
231
231
  }
@@ -373,7 +373,7 @@ function startLeaseClient(initialSocket, info, seams = {}) {
373
373
  });
374
374
  }
375
375
 
376
- // R1: annuncia la transizione di generazione sulla connessione viva PRIMA
376
+ // Annuncia la transizione di generazione sulla connessione viva PRIMA
377
377
  // che il supervisore apra il canale identita' della generazione nuova.
378
378
  function announceGeneration(generation) {
379
379
  if (!Number.isSafeInteger(generation) || generation < 0) {
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
  // lib/fleet/lease-routes.js — route /api/lease, superficie child della fetta 2b
3
- // (contratto rev1: B5 tre metodi distinti + D3 collegamento MCP↔leaseManager).
3
+ // (contract: three distinct methods + the MCP↔leaseManager link).
4
4
  //
5
5
  // Il bridge MCP di una cella (`nexuscrew mcp`) parla con l'HTTP API locale dietro
6
6
  // Bearer (canale nativo del bridge): queste route sono quel collegamento.
@@ -25,7 +25,7 @@ function leaseRoutes({
25
25
  fleetP, instanceId, now: () => Date.now(),
26
26
  // refresh/recovery hanno gia il proof child come authorizer nel body. Il
27
27
  // binding MCP, quando presentato, viene verificato fail-closed; l'assenza
28
- // preserva il canale nativo lease (D) che esisteva prima di G3.
28
+ // Preserva il canale nativo lease (D) che esisteva prima di .
29
29
  sharedRequired: false,
30
30
  });
31
31
 
@@ -51,7 +51,7 @@ function leaseRoutes({
51
51
  try {
52
52
  const fleet = await fleetP;
53
53
  if (!fleet || fleet.available !== true) return res.status(404).json({ error: 'fleet non disponibile' });
54
- // D3: il collegamento vive sul provider — senza leaseManager (lease
54
+ // The link lives vive sul provider — senza leaseManager (lease
55
55
  // disattivato) e' 501, non 500: la capability manca, non e' un guasto.
56
56
  if (!fleet.lease || typeof fleet.lease.childRegister !== 'function') {
57
57
  return res.status(501).json({ error: 'lease non disponibile su questo nodo' });
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  // Verifier per-installazione e proof HMAC del lease Live (fetta 2b, contratto
4
- // rev1: PREMESSA + B1/B4/B6/B7/B8 + C4/C5).
4
+ // Contract rev1: the premise and the items below.
5
5
  //
6
6
  // Modello (PREMESSA): la 2a usava un segreto condiviso simmetrico — il
7
7
  // supervisore presentava la capability cosi' com'e' e il server la confrontava.
@@ -9,25 +9,25 @@
9
9
  // segreto, il supervisore/child presenta un proof firmato con claims ed expiry.
10
10
  // Non e' la 2a con un giro in piu': e' un modello di autorizzazione diverso.
11
11
  //
12
- // - B7: la chiave verifier vive in un file DEDICATO separato 0o600, distinto
12
+ // La chiave verifier vive in un file DEDICATO separato 0o600, distinto
13
13
  // dai token di liveness per-cella e dal segreto del bridge audio. «Un solo
14
14
  // segreto» significa una sola chiave verifier, non un solo file segreto nel
15
15
  // sistema.
16
- // - C5: lo stato durevole contiene l'identificativo e l'impronta, MAI il
16
+ // Lo stato durevole contiene l'identificativo e l'impronta, MAI il
17
17
  // segreto — forma gia' usata da vl-node (PendingEnrollment). Il keyId e'
18
18
  // DERIVATO dall'impronta (sha256 della chiave): non esiste uno stato da
19
19
  // tenere sincronizzato con la chiave, e il meta su disco e' diagnostica.
20
- // - B4: la codifica canonica e' length-prefixed con proofKind come primo tag.
20
+ // La codifica canonica e' length-prefixed con proofKind come primo tag.
21
21
  // La canonizzazione JSON e' fragile: due serializzatori onesti producono
22
22
  // byte diversi. Il length-prefixing dichiara il confine di ogni campo, non
23
23
  // lo deduce da un separatore.
24
- // - B8: expiry = issuedAt + 60s, calcolabile all'emissione. «Ultimo-live+60»
24
+ // Expiry = issuedAt + 60s, calcolabile all'emissione. «Ultimo-live+60»
25
25
  // e' la proprieta' che si vuole, non la formula che si scrive: non e'
26
26
  // calcolabile nel momento in cui il proof va firmato.
27
- // - C4: fail-closed sulla verifica. La verifica prova TUTTE le chiavi vive
27
+ // Fail-closed sulla verifica. La verifica prova TUTTE le chiavi vive
28
28
  // (oggi una sola: la rotazione e' sospesa per scelta dichiarata, contratto
29
- // C3; la forma e' gia' quella a due chiavi di C2).
30
- // - C6: la verifica rende osservabile QUALE chiave ha firmato (keyId), cosi'
29
+ //; la forma e' gia' quella a due chiavi di).
30
+ // La verifica rende osservabile QUALE chiave ha firmato (keyId), cosi'
31
31
  // il momento in cui una chiave subentra resta leggibile dopo il fatto.
32
32
  //
33
33
  // Disciplina del file segreto: stessa di lib/audio/bridge-auth.js e
@@ -38,7 +38,7 @@ const fs = require('node:fs');
38
38
  const path = require('node:path');
39
39
  const crypto = require('node:crypto');
40
40
 
41
- // B8: vita di un proof emesso. Il refresh gira ogni 20s (REFRESH_MS): un proof
41
+ // Vita di un proof emesso. Il refresh gira ogni 20s (REFRESH_MS): un proof
42
42
  // da 60s lascia sempre al detentore >=2 presentazioni legittime di margine.
43
43
  const PROOF_TTL_MS = 60_000;
44
44
 
@@ -52,16 +52,16 @@ const KEY_ID_LEN = 16;
52
52
  const JTI_RE = /^[a-f0-9]{16,64}$/;
53
53
  const SIG_RE = /^[a-f0-9]{64}$/;
54
54
 
55
- // Campi firmati per proofKind, IN ORDINE, proofKind primo (B4). Per kind la
55
+ // Campi firmati per proofKind, IN ORDINE, proofKind primo. Per kind la
56
56
  // lista e' fissa e tutti i campi sono obbligatori e non vuoti: campo mancante e
57
57
  // campo vuoto non sono distinguibili nella canonica, quindi non esistono campi
58
- // opzionali. B6: nel kind 'lease' l'identita' del lease e' 'leaseId' — non
58
+ // Opzionali.: nel kind 'lease' l'identita' del lease e' 'leaseId' — non
59
59
  // identityKey, che legherebbe il lease all'identita' della cella.
60
60
  const KIND_FIELDS = Object.freeze({
61
61
  // tupla del supervisore: autorizza il reconnect all'endpoint stabile.
62
62
  lease: ['kind', 'cellId', 'launchEpoch', 'leaseId', 'generation', 'jti', 'issuedAt'],
63
- // tupla del child: autorizza register/refresh/recovery (B5). incarnationId e'
64
- // per-registration (B2), mai globale.
63
+ // Tupla del child: autorizza register/refresh/recovery. incarnationId e'
64
+ // Per-registration, mai globale.
65
65
  child: ['kind', 'cellId', 'incarnationId', 'jti', 'issuedAt'],
66
66
  });
67
67
 
@@ -69,7 +69,7 @@ function fingerprintOf(secret) {
69
69
  return crypto.createHash('sha256').update(String(secret), 'utf8').digest('hex');
70
70
  }
71
71
 
72
- // --- B4: codifica canonica ----------------------------------------------------
72
+ // -: codifica canonica ----------------------------------------------------
73
73
 
74
74
  // Ogni campo: u32 big-endian della lunghezza in byte UTF-8, poi i byte. Il
75
75
  // proofKind e' il primo tag: chiave di dominio della firma (un proof lease non
@@ -91,7 +91,7 @@ function claimsForKind(kind) {
91
91
  return fields;
92
92
  }
93
93
 
94
- // --- B7/C5: chiave per-installazione ------------------------------------------
94
+ // - : chiave per-installazione ------------------------------------------
95
95
 
96
96
  // Lettura no-follow (anti-symlink), stessa disciplina del bridge secret.
97
97
  function readKeySafe(fsImpl, keyPath) {
@@ -129,8 +129,8 @@ function loadOrCreateVerifier({ dir, fsImpl = fs, log = () => {}, now = Date.now
129
129
  try { fsImpl.chmodSync(keyPath, 0o600); } catch (_) {}
130
130
  // keyId derivato dall'impronta: la chiave porta con se' la propria identita'.
131
131
  const keyId = fingerprintOf(secret).slice(0, KEY_ID_LEN);
132
- // C5: il meta persiste id + impronta (MAI il segreto) per diagnostica e
133
- // osservabilita' (C6). Best-effort: se e' assente o divergente si riscrive;
132
+ // Il meta persiste id + impronta (MAI il segreto) per diagnostica e
133
+ // Osservabilita'. Best-effort: se e' assente o divergente si riscrive;
134
134
  // un fallimento di scrittura non invalida la chiave.
135
135
  try {
136
136
  const meta = { version: 1, keyId, fingerprint: fingerprintOf(secret), createdAt: now() };
@@ -172,11 +172,11 @@ function signProof(verifier, claims, { now = Date.now, jti = null } = {}) {
172
172
 
173
173
  function isNonEmptyString(v) { return typeof v === 'string' && v.length > 0; }
174
174
 
175
- // Fail-closed (C4): ogni difetto e' un motivo, non un'eccezione. `verifiers` e'
176
- // la lista delle chiavi vive (oggi una; C2-ready per due). `expect` porta i
175
+ // Fail-closed: ogni difetto e' un motivo, non un'eccezione. `verifiers` e'
176
+ // La lista delle chiavi vive (oggi una; -ready per due). `expect` porta i
177
177
  // claims che il chiamante gia' conosce: la firma prova il resto.
178
178
  // `graceMs` (default 0) allarga la finestra di accettazione DOPO la scadenza:
179
- // e' la finestra di recovery del child (B5) — un proof la cui firma e' valida e
179
+ // E' la finestra di recovery del child — un proof la cui firma e' valida e
180
180
  // la cui scadenza e' recente NON e' una credenziale rubata riportata in vita, e'
181
181
  // un detentore che ha saltato i refresh. Ogni altro check resta invariato.
182
182
  function verifyProof(verifiers, candidate, { now = Date.now, expect = {}, graceMs = 0 } = {}) {
@@ -193,7 +193,7 @@ function verifyProof(verifiers, candidate, { now = Date.now, expect = {}, graceM
193
193
  const issuedAt = Number(candidate.issuedAt);
194
194
  if (!Number.isSafeInteger(issuedAt)) return { ok: false, reason: 'malformed' };
195
195
  const expiresAt = Number(candidate.expiresAt);
196
- // B8: expiresAt non e' un campo qualunque: deve essere ESATTAMENTE
196
+ // ExpiresAt non e' un campo qualunque: deve essere ESATTAMENTE
197
197
  // issuedAt + PROOF_TTL_MS. Manometterlo non estende la vita del proof.
198
198
  if (!Number.isSafeInteger(expiresAt) || expiresAt !== issuedAt + PROOF_TTL_MS) {
199
199
  return { ok: false, reason: 'malformed' };
@@ -210,7 +210,7 @@ function verifyProof(verifiers, candidate, { now = Date.now, expect = {}, graceM
210
210
  if (expect.launchEpoch !== undefined && candidate.launchEpoch !== expect.launchEpoch) return { ok: false, reason: 'launchEpoch' };
211
211
  if (expect.leaseId !== undefined && candidate.leaseId !== expect.leaseId) return { ok: false, reason: 'leaseId' };
212
212
  if (expect.incarnationId !== undefined && candidate.incarnationId !== expect.incarnationId) return { ok: false, reason: 'incarnationId' };
213
- // Firma contro OGNI chiave viva: la prima che passa vince (C2/C6).
213
+ // Firma contro OGNI chiave viva: la prima che passa vince (/).
214
214
  const canonical = canonicalProofFields(fields.map((f) => candidate[f]));
215
215
  for (const v of verifiers) {
216
216
  if (!v || typeof v.secret !== 'string') continue;