@mmmbuto/nexuscrew 0.9.29 → 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 (77) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/docs/LIVE_PROMPT.md +1 -1
  3. package/docs/NOTIFICATIONS.md +37 -0
  4. package/frontend/dist/assets/index-BbcjoZS0.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 +5 -5
  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/files/routes.js +1 -1
  21. package/lib/fleet/boot.js +2 -2
  22. package/lib/fleet/builtin.js +15 -15
  23. package/lib/fleet/causes.js +1 -1
  24. package/lib/fleet/cell-exec.js +23 -23
  25. package/lib/fleet/cell-hold.js +6 -6
  26. package/lib/fleet/cell-lease-server.js +106 -106
  27. package/lib/fleet/cell-lease.js +21 -21
  28. package/lib/fleet/definitions.js +10 -10
  29. package/lib/fleet/identity-authority.js +1 -1
  30. package/lib/fleet/launch-broker.js +13 -13
  31. package/lib/fleet/launch.js +15 -15
  32. package/lib/fleet/lease-client.js +23 -23
  33. package/lib/fleet/lease-routes.js +3 -3
  34. package/lib/fleet/lease-verifier.js +22 -22
  35. package/lib/fleet/managed.js +35 -35
  36. package/lib/fleet/prompt-delivery.js +13 -13
  37. package/lib/fleet/routes.js +15 -10
  38. package/lib/fleet/runtime.js +24 -24
  39. package/lib/live-host/bridge.js +36 -36
  40. package/lib/live-host/routes.js +2 -2
  41. package/lib/live-host/store.js +4 -4
  42. package/lib/mcp/server.js +14 -14
  43. package/lib/mcp/tools.js +5 -5
  44. package/lib/nodes/access-presets.js +258 -0
  45. package/lib/nodes/commands.js +198 -4
  46. package/lib/nodes/store.js +152 -9
  47. package/lib/nodes/tunnel.js +7 -7
  48. package/lib/notify/ask-answer-service.js +184 -0
  49. package/lib/notify/ask-receipts.js +197 -0
  50. package/lib/notify/ask-relay.js +152 -0
  51. package/lib/notify/asks.js +28 -3
  52. package/lib/notify/event-feed-acl.js +95 -0
  53. package/lib/notify/event-feed-asks-routes.js +144 -0
  54. package/lib/notify/event-feed-client.js +434 -0
  55. package/lib/notify/event-feed-history.js +115 -0
  56. package/lib/notify/event-feed-producers.js +175 -0
  57. package/lib/notify/event-feed-routes.js +254 -0
  58. package/lib/notify/event-feed.js +328 -0
  59. package/lib/notify/events.js +1 -1
  60. package/lib/notify/notifier.js +3 -3
  61. package/lib/notify/push-relay.js +240 -0
  62. package/lib/notify/push.js +8 -8
  63. package/lib/notify/routes.js +62 -23
  64. package/lib/proxy/federation.js +74 -3
  65. package/lib/proxy/node-proxy.js +13 -13
  66. package/lib/proxy/panel-auth.js +4 -4
  67. package/lib/proxy/panel-proxy.js +2 -2
  68. package/lib/proxy/resource-acl.js +145 -0
  69. package/lib/server.js +372 -39
  70. package/lib/settings/pairing-coordinator.js +1 -1
  71. package/lib/settings/routes.js +45 -32
  72. package/lib/update/manager.js +1 -1
  73. package/lib/update/runner.js +1 -1
  74. package/lib/voice/transcribe.js +1 -1
  75. package/lib/ws/bridge.js +3 -3
  76. package/package.json +1 -1
  77. package/frontend/dist/assets/index-B_THfiL5.js +0 -167
@@ -1,5 +1,5 @@
1
1
  'use strict';
2
- // Minimal public CLI plus internal service/MCP entry points. [M6][R1]
2
+ // Minimal public CLI plus internal service/MCP entry points.
3
3
  // serve = foreground HTTP (+ --pidfile lifecycle su Termux/manuale).
4
4
  // start/stop/status = per-platform: linux (systemctl --user), mac (launchctl),
5
5
  // termux (nohup serve --pidfile + pidfile verificato; status boot-script vs running).
@@ -69,6 +69,10 @@ Usage:
69
69
  nexuscrew nodes share <name|nodeId> on|off [--json]
70
70
  nexuscrew nodes panel <name|nodeId> on|off
71
71
  nexuscrew nodes live-host <name|nodeId> on|off
72
+ nexuscrew nodes edit <name|nodeId> --access-role admin|user|nexushost
73
+ nexuscrew nodes edit <name|nodeId> --events-access on|off [...grant flags]
74
+ nexuscrew nodes edit <name|nodeId> --events-receive on|off
75
+ nexuscrew nodes access [--json]
72
76
  nexuscrew nodes invite --ssh TARGET [--ssh-port PORT] [--name SLUG]
73
77
  [--label TEXT] [--json]
74
78
  printf '%s' "$PAIRING_URL" | nexuscrew nodes pair|join
@@ -87,7 +91,7 @@ Lifecycle semantics:
87
91
 
88
92
  const CLI_VALUE_FLAGS = new Set([
89
93
  'label', 'ssh', 'ssh-port', 'autostart', 'visibility', 'selected',
90
- 'name', 'local-name', 'local-label', 'identity-file', 'port', 'dir',
94
+ 'name', 'local-name', 'local-label', 'identity-file', 'port', 'dir', 'events-receive',
91
95
  ]);
92
96
 
93
97
  // valueFlags (Set|array opzionale): flag che consumano il token successivo nella
@@ -221,7 +225,7 @@ function quickSummary(result, opts = {}) {
221
225
  log('check nexuscrew doctor');
222
226
  }
223
227
 
224
- // Ruoli client/node dal config.json (default entrambi off — B0 li popola dal wizard UI).
228
+ // Ruoli client/node dal config.json (default entrambi off — li popola dal wizard UI).
225
229
  function readRoles(configPath) {
226
230
  try {
227
231
  const c = JSON.parse(fs.readFileSync(configPath, 'utf8'));
@@ -791,8 +795,8 @@ function url(opts = {}) {
791
795
  return { url: full, port, hasToken: !!token };
792
796
  }
793
797
 
794
- // token rotate (§4b(3)): scrittura atomica nuovo token + invalidazione reale delle
795
- // sessioni attive (restart se il service e' attivo). Il nuovo token NON si stampa.
798
+ // Token rotation: atomic write of the new token plus real invalidation of the
799
+ // active sessions (service restart when running). The new token is never printed.
796
800
  function tokenRotate(opts = {}) {
797
801
  const log = opts.log || console.log;
798
802
  const platform = opts.platform || detectPlatform();
@@ -1017,7 +1021,7 @@ function update(opts = {}) {
1017
1021
  return { updated: true, running, restarted: running, bootDefinitions: healed, code: 0 };
1018
1022
  }
1019
1023
 
1020
- // B4.3 — fleet-boot companion: avvia le celle boot:true del provider selezionato.
1024
+ // Fleet boot companion: starts the boot:true cells of the selected provider.
1021
1025
  // dispatch e' sync (bin/nexuscrew.js non fa await), quindi runFleetBoot (async)
1022
1026
  // viene lanciato e il processo tenuto vivo (keepAlive) finche' non termina, poi
1023
1027
  // esce col code risultante. runFleetBoot e' la unit testabile (no process.exit);
@@ -1039,7 +1043,7 @@ async function runFleetBoot(opts = {}) {
1039
1043
  }
1040
1044
 
1041
1045
  // builtin: up() per ogni cella boot:true. READONLY fa fallire le up con 403 ->
1042
- // raccolte in failed[] -> exit 1 (nessun short-circuit speciale, design §9d).
1046
+ // collected in failed[] -> exit 1 (no special short-circuit by design).
1043
1047
  const res = await bootCells(sel.fleet, { log });
1044
1048
  log(`fleet-boot: ${res.started.length} started, ${res.skipped.length} skipped, ${res.failed.length} failed`);
1045
1049
  for (const f of res.failed) log(` failed: ${f.cell} — ${f.reason}`);
@@ -1105,12 +1109,16 @@ async function dispatchNodes(rest, flags, opts = {}) {
1105
1109
  if (sub === 'list') return { code: nodesCmds.nodesList({
1106
1110
  ...opts, log, json: flags.json === true, direct: flags.direct === true,
1107
1111
  }).code };
1108
- if (sub === 'inspect' || sub === 'show') return { code: nodesCmds.nodesInspect({ ...opts, log, ref, json: flags.json === true }).code };
1112
+ if (sub === 'inspect' || sub === 'show') {
1113
+ const feedDiagnostics = await nodesCmds.fetchFeedDiagnostics(opts);
1114
+ return { code: (await nodesCmds.nodesInspect({ ...opts, log, ref, json: flags.json === true, feedDiagnostics })).code };
1115
+ }
1109
1116
  if (sub === 'doctor') {
1110
1117
  const view = nodesCmds.loadPeerInventory(opts);
1111
1118
  let peers = view.nodes;
1119
+ const feedDiagnostics = await nodesCmds.fetchFeedDiagnostics(opts);
1112
1120
  if (ref) {
1113
- const inspected = nodesCmds.nodesInspect({ ...opts, log: () => {}, ref });
1121
+ const inspected = await nodesCmds.nodesInspect({ ...opts, log: () => {}, ref });
1114
1122
  if (inspected.code !== 0) { log(`nodes doctor: nodo sconosciuto "${ref}"`); return { code: 1 }; }
1115
1123
  peers = [inspected.peer];
1116
1124
  }
@@ -1130,7 +1138,15 @@ async function dispatchNodes(rest, flags, opts = {}) {
1130
1138
  }
1131
1139
  if (flags.json === true) log(JSON.stringify({ nodeId: view.nodeId, checks }, null, 2));
1132
1140
  else if (checks.length === 0) log('nodes doctor: nessun peer diretto');
1133
- else for (const check of checks) log(`${check.name}: ${check.ok ? 'OK' : 'KO'} — ${check.result}${check.detail ? ` · ${check.detail}` : ''}`);
1141
+ else for (const check of checks) {
1142
+ log(`${check.name}: ${check.ok ? 'OK' : 'KO'} — ${check.result}${check.detail ? ` · ${check.detail}` : ''}`);
1143
+ // Feed section per direct peer: kill-switch, cursor, last failure,
1144
+ // counters for observability. Read-only, no secrets.
1145
+ const peer = peers.find((p) => (p.nodeId || null) === (check.nodeId || null) && p.name === check.name);
1146
+ if (peer && peer.kind === 'direct') {
1147
+ for (const line of nodesCmds.feedLinesFor(feedDiagnostics, peer.nodeId)) log(` ${line}`);
1148
+ }
1149
+ }
1134
1150
  return { code: checks.some((check) => !check.ok) ? 1 : 0 };
1135
1151
  }
1136
1152
  if (sub === 'identity') {
@@ -1151,7 +1167,32 @@ async function dispatchNodes(rest, flags, opts = {}) {
1151
1167
  }
1152
1168
  if (flags.visibility !== undefined) patch.visibility = flags.visibility;
1153
1169
  if (flags.selected !== undefined) patch.selected = String(flags.selected).split(',').map((x) => x.trim()).filter(Boolean);
1154
- return { code: nodesCmds.nodesEdit({ ...opts, log, ref, patch }).code };
1170
+ // Federated access: a preset sets the whole vector, the granular flags
1171
+ // refine one that was already set explicitly.
1172
+ if (flags['access-role'] !== undefined) patch.accessRole = String(flags['access-role']);
1173
+ for (const [flag, key] of [
1174
+ ['events-access', 'eventsAccess'],
1175
+ ['node-events-access', 'nodeEventsAccess'],
1176
+ ['ask-reply-access', 'askReplyAccess'],
1177
+ ['files-read-access', 'filesReadAccess'],
1178
+ ['peer-operator-access', 'peerOperatorAccess'],
1179
+ ]) {
1180
+ if (flags[flag] === undefined) continue;
1181
+ try { patch[key] = boolFlag(flags[flag], `--${flag}`); }
1182
+ catch (e) { log(e.message); return { code: 1 }; }
1183
+ }
1184
+ // The client's OWN choice to import a peer's feed (never an owner grant):
1185
+ // on|off, same shape as the other boolean switches.
1186
+ if (flags['events-receive'] !== undefined) {
1187
+ try { patch.eventsReceive = boolFlag(flags['events-receive'], '--events-receive'); }
1188
+ catch (e) { log(e.message); return { code: 1 }; }
1189
+ }
1190
+ const expectedRevision = flags['access-revision'] === undefined ? undefined : Number(flags['access-revision']);
1191
+ if (expectedRevision !== undefined && !Number.isInteger(expectedRevision)) {
1192
+ log('nodes edit: --access-revision vuole un intero (la revision letta da `nodes access`)');
1193
+ return { code: 1 };
1194
+ }
1195
+ return { code: nodesCmds.nodesEdit({ ...opts, log, ref, patch, expectedRevision }).code };
1155
1196
  }
1156
1197
  if (sub === 'rename') {
1157
1198
  if (flags.label === undefined) { log('nodes rename: --label TEXT obbligatorio'); return { code: 1 }; }
@@ -1202,6 +1243,12 @@ async function dispatchNodes(rest, flags, opts = {}) {
1202
1243
  }
1203
1244
  return { code: nodesCmds.nodesEdit({ ...opts, log, ref, patch: { liveHostAccess: arg === 'on' } }).code };
1204
1245
  }
1246
+ // Inventario read-only delle concessioni: chi puo' fare cosa su QUESTO nodo,
1247
+ // con la revisione della policy. E' il punto di partenza della migrazione
1248
+ // esplicita: i peer non configurati restano negati finche' non lo sono.
1249
+ if (sub === 'access') {
1250
+ return { code: nodesCmds.nodesAccess({ ...opts, log, json: flags.json === true }).code };
1251
+ }
1205
1252
  if (sub === 'remove') {
1206
1253
  if (flags.yes !== true) { log('nodes remove: conferma richiesta con --yes'); return { code: 1 }; }
1207
1254
  return { code: nodesCmds.nodesRemove({ ...opts, log, ref }).code };
@@ -1303,7 +1350,7 @@ function dispatch(argv, opts = {}) {
1303
1350
  const { flags, rest } = parseFlags(argv, CLI_VALUE_FLAGS);
1304
1351
  const cmd = rest[0];
1305
1352
 
1306
- // R29 — help come flag, deciso qui e solo qui: dopo parseFlags `--help`
1353
+ // Help-as-flag handling come flag, deciso qui e solo qui: dopo parseFlags `--help`
1307
1354
  // finisce in flags e `-h` resta come posizionale dopo il sottocomando; in
1308
1355
  // entrambi i casi il sottocomando NON deve essere eseguito. Prima di questa
1309
1356
  // guardia `nexuscrew init --help` eseguiva init davvero: rigenerava i plist,
package/lib/cli/doctor.js CHANGED
@@ -1,5 +1,5 @@
1
1
  'use strict';
2
- // nexuscrew doctor: auto-diagnosi (design §3, §7). [A2]
2
+ // nexuscrew doctor: self-diagnosis entry point.
3
3
  // Struttura ESTENSIBILE: una lista di check-fn, ognuna ritorna
4
4
  // { name, ok, warn?, detail? }. SSH è un requisito locale; la policy del
5
5
  // server remoto si prova soltanto tentando il forwarding reale.
@@ -276,7 +276,7 @@ function checkTmuxSurvival(platform, execImpl) {
276
276
  };
277
277
  }
278
278
 
279
- // ssh client presente su PATH: prerequisito dei tunnel multi-node (design §4).
279
+ // ssh client on PATH: prerequisite for the multi-node tunnels.
280
280
  function checkSshClient(existsImpl) {
281
281
  return existsImpl('ssh')
282
282
  ? { name: 'OpenSSH transport', ok: true, detail: 'ssh presente · USATO dal supervisor NexusCrew' }
@@ -424,7 +424,7 @@ function checkTmuxServerCwd(platform, execImpl, opts = {}) {
424
424
 
425
425
  // Inspect only the single tmux global environment key required by
426
426
  // termux-exec. The value is validated and then discarded; it is never logged.
427
- // This distinguishes a healthy server from RC-2 (stale/no preload) and RC-7
427
+ // This distinguishes a healthy server from a stale run (no preload) and one
428
428
  // (present but rejected by the trust boundary) without killing any session.
429
429
  function checkTmuxServerTermuxPreload(runtimeEnv, execImpl, opts = {}) {
430
430
  const env = runtimeEnv || process.env;
@@ -448,13 +448,13 @@ function checkTmuxServerTermuxPreload(runtimeEnv, execImpl, opts = {}) {
448
448
  };
449
449
  }
450
450
 
451
- // Check MCP identity (P0). NON-FAILING per costrutto: `ok` è sempre true, così
451
+ // Check MCP identity. NON-FAILING per costrutto: `ok` è sempre true, così
452
452
  // chi usa NexusCrew solo come PWA (nessuna integrazione MCP) non vede mai il
453
453
  // doctor andare in FAIL solo per env MCP assente. Il check osserva SOLO la
454
454
  // presence delle env var di identità nel processo che lancia `doctor` (nessuna
455
455
  // lettura di ~/.codex/config.toml, cache private o config MCP).
456
456
  //
457
- // LIMITE DOCUMENTATO (P0): `doctor` non gira dentro il server MCP stdio e non può
457
+ // LIMITE DOCUMENTATO: `doctor` non gira dentro il server MCP stdio e non può
458
458
  // distinguere in modo portatile "MCP configurato" da "PWA-only". Il WARN è quindi
459
459
  // conservativo e informativo: segnala l'assenza di identità osservabile a chi sta
460
460
  // configurando l'MCP, senza rompere l'utente PWA-only (per il quale ok resta true).
@@ -1,5 +1,5 @@
1
1
  'use strict';
2
- // B4.3 — Service companion (boot) generation + migration gate. Design §4c / §9b.
2
+ // Service companion (boot) generation and migration gate.
3
3
  //
4
4
  // generateFleetService({platform, nodeBin, entryPath}) genera il contenuto del
5
5
  // service UNICO di boot (NON una unit per cella) che al boot esegue
@@ -44,7 +44,7 @@ function generateFleetService(opts) {
44
44
  function generateFleetLinux(opts) {
45
45
  const home = opts.home || os.homedir();
46
46
  const { nodeBin, entryPath } = opts;
47
- // Parita' di hardening con service.js (M3): reject char non gestibili in systemd.
47
+ // Parita' di hardening con service.js: reject char non gestibili in systemd.
48
48
  assertSystemdSafe('home', home);
49
49
  assertSystemdSafe('nodeBin', nodeBin);
50
50
  assertSystemdSafe('entryPath', entryPath);
@@ -194,7 +194,7 @@ function selectProviderModeSync(cfg = {}) {
194
194
 
195
195
  // --- Install (speculare a installService di lib/cli/service.js) ---
196
196
  // Stesso hardening di service.js: no-symlink atomic (lstat + tmp+rename), failure
197
- // collection (NON ingoiare activation — M1), execImpl iniettabile (test). Differenze
197
+ // Collection (NON ingoiare activation —), execImpl iniettabile (test). Differenze
198
198
  // companion:
199
199
  // - nomi 'nexuscrew-fleet.*' + mode termux 0o755 (boot script eseguibile)
200
200
  // - linux: daemon-reload + enable, NESSUN restart (Type=oneshot -> parte al boot)
@@ -209,7 +209,7 @@ function fleetInstallPath(platform, home) {
209
209
  }
210
210
 
211
211
  function fleetFileMode(platform) {
212
- if (platform === 'termux') return 0o755; // boot script eseguibile (design §4c: chmod 755)
212
+ if (platform === 'termux') return 0o755; // boot script must stay executable (chmod 755)
213
213
  return 0o644; // systemd unit + launchd plist (come service.js)
214
214
  }
215
215
 
@@ -237,7 +237,7 @@ function fleetInstallCommands(platform, target, ctx) {
237
237
 
238
238
  // Install no-symlink + atomic rename (come service.js). execImpl iniettabile per test;
239
239
  // default execFileSync (argv diretto, MAI shell string). Su activation fallita il file
240
- // e' PRESERVATO e le failure raccolte per diagnosi (M1: non si ingoia, non si rollback).
240
+ // E' PRESERVATO e le failure raccolte per diagnosi (it is never swallowed, non si rollback).
241
241
  // da revisione: `activate: false` (speculare a installService) scrive la definizione SENZA
242
242
  // toccare il service manager — per chi rigenera a meta' aggiornamento: il companion
243
243
  // e' RunAtLoad/oneshot, un bootstrap adesso eseguirebbe le celle boot:true mentre
@@ -255,7 +255,7 @@ function installFleetService(platform, content, ctx, {
255
255
  const target = ctx.installPath || fleetInstallPath(platform, home);
256
256
  const mode = fleetFileMode(platform);
257
257
 
258
- // lstat: reject symlink preesistente (no-symlink atomic, parita' service.js [M3])
258
+ // Lstat: reject symlink preesistente (no-symlink atomic, parita' service.js)
259
259
  try {
260
260
  const st = fs.lstatSync(target);
261
261
  if (st.isSymbolicLink()) {
@@ -288,7 +288,7 @@ function installFleetService(platform, content, ctx, {
288
288
  return { target, mode, written: true, failures: [], skippedActivation: skipped };
289
289
  }
290
290
 
291
- // exec service manager — raccogli failure per diagnosi (NON ingoiare, M1)
291
+ // Exec service manager — raccogli failure per diagnosi (NON ingoiare)
292
292
  const failures = [];
293
293
  if (platform === 'mac') {
294
294
  failures.push(...reinstallLaunchAgent(target, 'com.mmmbuto.nexuscrew-fleet', ctx, {
package/lib/cli/init.js CHANGED
@@ -1,10 +1,10 @@
1
1
  'use strict';
2
- // nexuscrew init: orchestrazione setup portatile. [B2][M8][R4]
2
+ // Nexuscrew init: orchestrazione setup portatile.
3
3
  // detectPlatform -> prereq (Node>=18 abort, tmux abort-before-service) ->
4
4
  // migration rule (porta da service esistente) -> config.json (preserva) ->
5
5
  // token (preserva) -> NexusFiles -> generateService -> installService (skip dry-run) ->
6
- // [B4.3] fleet companion (solo se provider builtin + gate ok; READONLY/dry-run skip;
7
- // mai fa fallire l'init principale) -> print URL #token. Termux:boot best-effort.
6
+ // fleet companion (only when the provider is builtin and the gate passes;
7
+ // never fails the main init) -> print URL #token. Termux boot: best-effort.
8
8
  const fs = require('node:fs');
9
9
  const os = require('node:os');
10
10
  const path = require('node:path');
@@ -12,7 +12,7 @@ const crypto = require('node:crypto');
12
12
  const { detectPlatform, nodeBin, repoRoot, uid } = require('./platform.js');
13
13
  const { loadOrCreateToken } = require('../auth/token.js');
14
14
  const { generateService, installService, fileMode, installPath: svcInstallPath } = require('./service.js');
15
- // B4.3 — companion di boot del fleet (design §4c/§9b/§9d). Seam iniettabili per test.
15
+ // Fleet boot companion. Injectable seams for tests.
16
16
  const {
17
17
  generateFleetService, installFleetService, migrationGate,
18
18
  selectProviderModeSync, fleetFileMode,
@@ -103,7 +103,7 @@ function ensureFleetDefaults(opts = {}) {
103
103
  return { path: fleetDefsPath, created: Boolean(creato), enabled: true };
104
104
  }
105
105
 
106
- // Migration rule (B2): se non c'è config.json, parse la porta dal service file esistente.
106
+ // Migration rule: se non c'è config.json, parse la porta dal service file esistente.
107
107
  function readExistingPort(platform, home, installPathOverride) {
108
108
  const p = installPathOverride || svcInstallPath(platform, home);
109
109
  let raw;
@@ -139,7 +139,7 @@ function runInit(opts = {}) {
139
139
  const log = opts.log || (() => {});
140
140
  const tmuxOk = opts.tmuxOk !== undefined ? opts.tmuxOk : haveTmux(opts.tmuxBin || 'tmux', opts.env || process.env);
141
141
 
142
- // prereq Node (abort before any write) [M8]
142
+ // Prereq Node (abort before any write)
143
143
  if (nodeMajor() < 18) {
144
144
  throw new Error(`Node >= 18 richiesto (attuale ${process.versions.node}). Aggiorna Node prima di init.`);
145
145
  }
@@ -161,7 +161,7 @@ function runInit(opts = {}) {
161
161
  if (migrated) { port = migrated; actions.push(`migration: porta ${port} letta dal service esistente`); }
162
162
  }
163
163
 
164
- // config.json (scrivi se non esiste; preserva se esiste) [B2]
164
+ // Config.json (scrivi se non esiste; preserva se esiste)
165
165
  if (!dryRun) {
166
166
  fs.mkdirSync(configDir, { recursive: true });
167
167
  if (!fs.existsSync(configPath)) {
@@ -198,7 +198,7 @@ function runInit(opts = {}) {
198
198
  }
199
199
  if (!port) port = 41820;
200
200
 
201
- // token (preserva esistente) [M4]
201
+ // Token (preserva esistente)
202
202
  let token;
203
203
  if (!dryRun) {
204
204
  token = loadOrCreateToken(tokenPath);
@@ -213,7 +213,7 @@ function runInit(opts = {}) {
213
213
  actions.push(`files root ${filesRoot}`);
214
214
  }
215
215
 
216
- // nodes.json (B0): nodeId STABILE per installazione (generato qui se manca) +
216
+ // Nodes.json: nodeId STABILE per installazione (generato qui se manca) +
217
217
  // migrazione esplicita guarded dei nodes legacy da config.json. READONLY/dry-run
218
218
  // saltano; ogni errore -> WARN, MAI far fallire l'init.
219
219
  if (!dryRun && process.env.NEXUSCREW_READONLY !== '1' && !opts.readonly) {
@@ -244,7 +244,7 @@ function runInit(opts = {}) {
244
244
  port, home, uid: uid(),
245
245
  installPath: opts.installPath,
246
246
  };
247
- // 1-bis (R23): la STESSA classe di difetto del companion di boot — nodeBin
247
+ // 1-bis: la STESSA classe di difetto del companion di boot — nodeBin
248
248
  // e entry finiscono versionati nell'unit/plist del servizio per-nodo. Si
249
249
  // risolve qui, alla scrittura, e si dichiara cio' che non ha alias stabile:
250
250
  // i due punti di scrittura non devono divergere (correggerne uno solo
@@ -262,7 +262,7 @@ function runInit(opts = {}) {
262
262
  } else if (dryRun) {
263
263
  actions.push(`DRY-RUN service (${platform}) generato, NON installato`);
264
264
  } else if (!tmuxOk) {
265
- // tmux mancante: abort before service install (config/token gia' creati) [M8]
265
+ // Tmux mancante: abort before service install (config/token gia' creati)
266
266
  // Se il chiamante ha imposto tmuxOk direttamente (test/seam), non
267
267
  // abbiamo l'informazione per distinguere assenza da verifica bloccata:
268
268
  // il messaggio resta quello storico.
@@ -285,24 +285,24 @@ function runInit(opts = {}) {
285
285
  actions.push(skip);
286
286
  }
287
287
  if (r.failures && r.failures.length) {
288
- // file installato MA activation (systemctl/launchctl) fallita [M1]
288
+ // File installato MA activation (systemctl/launchctl) fallita
289
289
  installFailures.push({ component: 'service', phase: 'activation' });
290
290
  actions.push(`WARN: service file installato ${r.target} MA activation fallita: ${r.failures.map((f) => f.cmd).join('; ')} (file preservato, diagnosi)`);
291
291
  } else {
292
292
  actions.push(`service installed ${r.target} (mode 0${fileMode(platform).toString(8)})`);
293
293
  }
294
294
  } catch (e) {
295
- // failure: preserve file + diagnosi (no rollback) [M8]
295
+ // Failure: preserve file + diagnosi (no rollback)
296
296
  installFailures.push({ component: 'service', phase: 'install' });
297
297
  actions.push(`WARN: service install fallito: ${e.message} (file generati preservati)`);
298
298
  }
299
299
  }
300
300
 
301
- // --- B4.3 companion: service di boot del fleet (design §4c/§9b/§9d) ---
302
- // Il companion si installa SOLO se il provider e' builtin (§9b) e il migration
303
- // gate non rileva unit legacy cloud-cell@*.service abilitate (no doppio boot).
304
- // READONLY blocca ogni azione del companion (§9d). MAI far fallire l'init
305
- // principale: ogni errore del companion -> WARN action + return normale.
301
+ // --- Fleet boot companion service ---
302
+ // The companion installs ONLY when the provider is builtin and the migration
303
+ // gate finds no enabled legacy cloud-cell@*.service units (no double boot).
304
+ // READONLY blocks every companion action. Never let it fail the main init:
305
+ // any companion error -> WARN action + normal return.
306
306
  try {
307
307
  const readonly = process.env.NEXUSCREW_READONLY === '1' || opts.readonly;
308
308
  if (!installBoot) {
@@ -310,9 +310,9 @@ function runInit(opts = {}) {
310
310
  } else if (readonly) {
311
311
  actions.push('fleet companion: READONLY, non installato');
312
312
  } else {
313
- // provider mode: companion SOLO se builtin (§9b). selectProviderModeSync e'
314
- // il default SINCRONO (runInit e' sync: bin chiama process.exit dopo dispatch,
315
- // selectProvider async non sopravvive). Seam opts.selectProvider per i test.
313
+ // provider mode: companion ONLY when builtin. selectProviderModeSync is
314
+ // the synchronous default (runInit is sync: the bin calls process.exit after
315
+ // dispatch, an async selectProvider would not survive). Seam opts.selectProvider for tests.
316
316
  const selectProvider = opts.selectProvider || selectProviderModeSync;
317
317
  const fleetCfg = opts.fleetCfg || {
318
318
  home,
@@ -324,16 +324,16 @@ function runInit(opts = {}) {
324
324
  if (sel.mode !== 'builtin') {
325
325
  actions.push(`fleet companion: non installato (provider ${sel.mode})`);
326
326
  } else {
327
- // migration gate (§9b): seam opts.migrationGate iniettabile; default quello
328
- // di fleet-service.js (exec iniettabile via opts.execImpl). blocked -> NON
329
- // installare (mai doppio boot silenzioso).
327
+ // migration gate: seam opts.migrationGate injectable; the fleet-service.js
328
+ // one (injectable exec via opts.execImpl). Blocked -> do NOT
329
+ // install (never a silent double boot).
330
330
  const gate = (opts.migrationGate || migrationGate)({ exec: opts.execImpl, platform });
331
331
  if (gate.blocked) {
332
332
  actions.push(`WARN: fleet companion NON installato — migration gate bloccato: ${(gate.units || []).join(', ')} (${gate.remediation || 'disabilita le unit legacy cloud-cell@*.service'})`);
333
333
  } else if (dryRun) {
334
334
  actions.push('DRY-RUN fleet companion generato, NON installato');
335
335
  } else {
336
- // R23: i path del boot si risolvono alla SCRITTURA del servizio (mai
336
+ // I path del boot si risolvono alla SCRITTURA del servizio (mai
337
337
  // a ogni avvio): un alias stabile che risolva allo stesso file, se
338
338
  // c'e'; altrimenti il path attuale, DICHIARATO — un avviso adesso e'
339
339
  // meglio di un path che scade in silenzio (vedi stable-alias.js).
@@ -355,7 +355,7 @@ function runInit(opts = {}) {
355
355
  if (fr.skippedActivation) {
356
356
  actions.push(`fleet companion ${fr.target}: activation skipped (--no-activate)`);
357
357
  } else if (fr.failures && fr.failures.length) {
358
- // file installato MA activation (systemctl/launchctl) fallita [M1]
358
+ // File installato MA activation (systemctl/launchctl) fallita
359
359
  installFailures.push({ component: 'fleet-companion', phase: 'activation' });
360
360
  actions.push(`WARN: fleet companion file installato ${fr.target} MA activation fallita: ${fr.failures.map((f) => f.cmd).join('; ')} (file preservato, diagnosi)`);
361
361
  } else {
@@ -370,7 +370,7 @@ function runInit(opts = {}) {
370
370
  actions.push(`WARN: fleet companion fallito: ${e.message} (init principale prosegue)`);
371
371
  }
372
372
 
373
- // Termux:boot best-effort detection (R4)
373
+ // Termux:boot best-effort detection
374
374
  if (platform === 'termux' && installBoot) {
375
375
  const bootDir = path.join(home, '.termux', 'boot');
376
376
  const bootOk = fs.existsSync(bootDir);
@@ -2,7 +2,7 @@
2
2
  // Pidfile con verified kill: metadata {pid, cmd, startTs}; kill verifica cmd+pid
3
3
  // prima di signalare (no broad match by name). Il controllo pid+nascita riduce
4
4
  // il PID reuse a una finestra residua fra verifica e segnale — non lo elimina;
5
- // dettaglio e motivo nel commento sopra il kill in killPidfile. [R1]
5
+ // Dettaglio e motivo nel commento sopra il kill in killPidfile.
6
6
  // Primario su Termux (serve --pidfile); opzionale --manual su linux/mac.
7
7
  const fs = require('node:fs');
8
8
  const os = require('node:os');
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
  // Service generation per-platform: systemd --user (linux), launchd plist (mac),
3
- // Termux:boot script. Escaping per-platform + install no-symlink atomic. [M2][M3][B1][R2][R3]
3
+ // Termux:boot script. Escaping per-platform + install no-symlink atomic.
4
4
  const fs = require('node:fs');
5
5
  const os = require('node:os');
6
6
  const path = require('node:path');
@@ -42,7 +42,7 @@ function shellQuote(s) {
42
42
  }
43
43
 
44
44
  // systemd ExecStart non gestisce bene alcuni char (", $, ;, backtick, newline, apice)
45
- // nemmeno con escaping ad hoc: reject esplicito con errore chiaro. [M3]
45
+ // Nemmeno con escaping ad hoc: reject esplicito con errore chiaro.
46
46
  // space e % sono ok (escaped). Path repo normali non hanno questi char.
47
47
  const SYSTEMD_FORBIDDEN = /[";$`\n']/;
48
48
  function assertSystemdSafe(label, p) {
@@ -54,7 +54,7 @@ function assertSystemdSafe(label, p) {
54
54
  // --- Templates ---
55
55
 
56
56
  // Entry del servizio: di norma derivato da repoRoot; i punti di scrittura
57
- // che risolvono l'alias stabile (R23/1-bis, stable-alias.js) passano
57
+ // Che risolvono l'alias stabile (/1-bis, stable-alias.js) passano
58
58
  // l'entryPath gia' risolto. Contratto vecchio intatto: senza entryPath si
59
59
  // deriva da repoRoot come prima.
60
60
  function serviceEntryPath(ctx) {
@@ -69,7 +69,7 @@ function generateService(platform, ctx) {
69
69
  }
70
70
 
71
71
  function generateLinux(ctx) {
72
- assertSystemdSafe('repoRoot', ctx.repoRoot); // [M3] reject char non gestibili
72
+ assertSystemdSafe('repoRoot', ctx.repoRoot); // Reject char non gestibili
73
73
  assertSystemdSafe('nodeBin', ctx.nodeBin);
74
74
  // HOME is stable across init/repair/update. The runtime directory may be
75
75
  // atomically replaced; keeping it as the service cwd leaves the HTTP process
@@ -304,7 +304,7 @@ function reinstallLaunchAgent(target, serviceName, ctx, opts = {}) {
304
304
  }
305
305
  }
306
306
 
307
- // Install no-symlink + atomic rename. [M3]
307
+ // Install no-symlink + atomic rename.
308
308
  // - lstat target: reject symlink (no follow)
309
309
  // - write temp file nella stessa dir -> chmod mode -> atomic rename
310
310
  // - exec service manager (systemctl/launchctl); skip in dryRun
@@ -352,7 +352,7 @@ function installService(platform, content, ctx, {
352
352
  throw e;
353
353
  }
354
354
 
355
- // exec service manager — NON ingoiare failure (M1): raccogli per diagnosi
355
+ // Exec service manager — NON ingoiare failure: raccogli per diagnosi
356
356
  const cmds = installCommands(platform, target, ctx);
357
357
  const activation = planActivation(platform, target, ctx, { execImpl, activate });
358
358
  if (!activation.activate) {
@@ -1,5 +1,5 @@
1
1
  'use strict';
2
- // R23 — alias stabili per i path del servizio di boot.
2
+ // Alias stabili per i path del servizio di boot.
3
3
  //
4
4
  // Il companion di boot scrive nodeBin e entryPath LETTERALMENTE nel plist
5
5
  // launchd (ProgramArguments[0]) e nell'unit systemd (ExecStart=). Quei path
@@ -17,7 +17,7 @@
17
17
  // Se c'e', scrivere quello; se no, scrivere il path attuale e DICHIARARLO:
18
18
  // un avviso alla scrittura e' meglio di un path che scade in silenzio.
19
19
  //
20
- // Tre vincoli non negoziabili (briefing R23):
20
+ // Tre vincoli non negoziabili (briefing):
21
21
  // 1. La verifica «e' lo stesso file» si fa alla SCRITTURA del servizio, MAI a
22
22
  // ogni avvio: dopo l'upgrade il symlink DEVE puntare a un node diverso —
23
23
  // e' l'effetto voluto. Un controllo di identita' permanente si
@@ -116,7 +116,7 @@ function resolveBootPaths({ nodeBin, entryPath, env = process.env, realpath } =
116
116
  // morto: il suo criterio e' «alias che realpath allo STESSO file», e un file
117
117
  // morto non e' lo stesso file di nessuno. Il criterio qui e' diverso e
118
118
  // dichiarato: un path VIVO — l'alias stabile dello stesso file quando
119
- // l'attuale vive (R23 intatto), altrimenti il primo candidato stabile che
119
+ // L'attuale vive (intatto), altrimenti il primo candidato stabile che
120
120
  // ESISTE, con la dichiarazione; se non c'e' niente di vivo, il path resta e
121
121
  // la dichiarazione dice di rilanciare init. `env`, `realpath` e `exists`
122
122
  // iniettabili per test.
@@ -127,7 +127,7 @@ function resolveLiveBootPaths({ nodeBin, entryPath, env = process.env, realpath,
127
127
  const revive = (current, candidates, what) => {
128
128
  try {
129
129
  realpathImpl(current);
130
- return null; // vivo: decide il criterio R23 di resolveBootPaths
130
+ return null; // Vivo: decide il criterio di resolveBootPaths
131
131
  } catch (_) { /* morto su disco: cerca una alternativa viva */ }
132
132
  for (const candidate of Array.isArray(candidates) ? candidates : []) {
133
133
  if (existsImpl(candidate)) {
package/lib/cli/url.js CHANGED
@@ -1,5 +1,5 @@
1
1
  'use strict';
2
- // URL/token helpers per la CLI unificata (design §3). [A2]
2
+ // URL/token helpers for the unified CLI.
3
3
  // - resolvePaths: config/token path home-aware (seam per test con HOME temporanea).
4
4
  // - readToken: legge il token esistente SENZA crearlo (no-follow symlink), null se assente.
5
5
  // - loadPort: porta corrente (opts > env > config.json > default 41820).
package/lib/config.js CHANGED
@@ -17,7 +17,7 @@ function baseDefaults() {
17
17
  return {
18
18
  bind: '127.0.0.1',
19
19
  port: 41820,
20
- // Seconda porta loopback, SOLO per /panel/* (P0 sicurezza 2026-08-16):
20
+ // Seconda porta loopback, SOLO per /panel/* (sicurezza 2026-08-16):
21
21
  // un pannello vive su un'origin diversa dal control plane — la porta fa
22
22
  // parte dell'origin, quindi un iframe cross-porta non legge il
23
23
  // localStorage del padre, qualunque JS esegua il container. Nessuna API
@@ -57,14 +57,14 @@ function baseDefaults() {
57
57
  // Il manager aggiorna solo verso una semver superiore: mai downgrade.
58
58
  autoUpdate: true,
59
59
  sessionPresets: {},
60
- // Ponte Live (fetta 3, rev5 MC0): isolabile — a false il ponte non si
60
+ // Ponte Live (fetta 3, rev5): isolabile — a false il ponte non si
61
61
  // connette mai e ogni avvio di Live resta sul comportamento standard.
62
62
  liveBridgeEnabled: true,
63
63
  // Socket di controllo dell'app-server (misurato 2026-08-15:
64
64
  // $CODEX_HOME/app-server-control/app-server-control.sock, WebSocket sopra
65
65
  // unix socket). Non cablare mai una porta al posto di questo path.
66
66
  liveBridgeSocketPath: path.join(os.homedir(), '.codex', 'app-server-control', 'app-server-control.sock'),
67
- // MC1.5: limite dichiarato per OGNI fase del ponte (GET designazione e
67
+ // Limite dichiarato per OGNI fase del ponte (GET designazione e
68
68
  // sessione sul socket). Oltre questo la Live parte senza puntamento.
69
69
  liveBridgeTimeoutMs: 1500,
70
70
  // PTY grace is finite and configurable for mobile handovers. The caps keep
@@ -82,7 +82,37 @@ function configJsonPath() {
82
82
  return process.env.NEXUSCREW_CONFIG_FILE || path.join(os.homedir(), '.nexuscrew', 'config.json');
83
83
  }
84
84
 
85
- // Legge ~/.nexuscrew/config.json se esiste (nuovo, B2). {} se assente/malformato.
85
+ // Legge ~/.nexuscrew/config.json se esiste (nuovo). {} se assente/malformato.
86
+ // federation.events.enabled — the node-level kill switch for the event feed.
87
+ // STRICT boolean: only the literal false disables. A truthy string or any
88
+ // other type never enables anything beyond the default, and never disables by
89
+ // accident: "false" (string) is not a decision, it is noise, and the default
90
+ // holds. Without explicit grants nothing is enabled either way — the switch
91
+ // only stops what grants already allow.
92
+ function eventsEnabledFlag(raw, log = console.warn) {
93
+ // Fail-closed strictness: only the literal true/false is a decision. A
94
+ // missing value keeps the default (enabled); ANY other type — "false" as a
95
+ // string, 0, null, an object — is invalid config, is treated as DISABLED and
96
+ // is reported: silently enabling on garbage is how switches lie.
97
+ if (raw === undefined) return true;
98
+ if (raw === true) return true;
99
+ if (raw === false) return false;
100
+ try { log('config: federation.events.enabled must be a boolean; invalid value treated as disabled'); } catch (_) {}
101
+ return false;
102
+ }
103
+
104
+ function readFederationEventsEnabled(opts = {}) {
105
+ if (typeof opts.federationEventsEnabled === 'boolean') return opts.federationEventsEnabled;
106
+ let cfg = {};
107
+ try {
108
+ const raw = fs.readFileSync(opts.configJsonPath || configJsonPath(), 'utf8');
109
+ cfg = JSON.parse(raw) || {};
110
+ } catch (_) { cfg = {}; }
111
+ const fed = cfg && typeof cfg.federation === 'object' && cfg.federation ? cfg.federation : null;
112
+ const events = fed && typeof fed.events === 'object' && fed.events ? fed.events : null;
113
+ return eventsEnabledFlag(events ? events.enabled : undefined);
114
+ }
115
+
86
116
  function readConfigJson() {
87
117
  try {
88
118
  const raw = fs.readFileSync(configJsonPath(), 'utf8');
@@ -134,7 +164,7 @@ function envOverrides() {
134
164
  return e;
135
165
  }
136
166
 
137
- // Precedence: baseDefaults < config.json < env < opts. [B2]
167
+ // Precedence: baseDefaults < config.json < env < opts.
138
168
  // Le credenziali authority vivono in due file 0600 nella dir
139
169
  // dell'authority (default ~/.nexuscrew/identity-authority). Se esistono vengono
140
170
  // caricate qui, senza MAI loggare i valori: builtin costruisce l'authority
@@ -195,4 +225,4 @@ function defaults() {
195
225
  return { ...baseDefaults(), ...envOverrides() };
196
226
  }
197
227
 
198
- module.exports = { assertLoopback, baseDefaults, readConfigJson, loadConfig, defaults, LOOPBACK, configJsonPath };
228
+ module.exports = { assertLoopback, baseDefaults, readConfigJson, loadConfig, defaults, LOOPBACK, configJsonPath, eventsEnabledFlag, readFederationEventsEnabled };
@@ -55,7 +55,7 @@ function filesRoutes({
55
55
  // paste=false (tasto allegati del composer): il client appende il path al
56
56
  // testo del composer — niente scrittura PTY. Default = incolla (FilesPanel).
57
57
  const wantPaste = String((req.body && req.body.paste) || '') !== 'false';
58
- // R31-A2: paste RICHIESTO ma non riuscito non e' un 200 con pasted:false —
58
+ // : paste RICHIESTO ma non riuscito non e' un 200 con pasted:false —
59
59
  // tre cause collassavano in quell'unica etichetta muta (non richiesto /
60
60
  // tmux non ha ricevuto / testo respinto) e nessuno, risposta o UI, poteva
61
61
  // accorgersene. Il vocabolario e' quello del ramo answer degli ask
package/lib/fleet/boot.js CHANGED
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
- // B4.3 — Service companion boot. bootCells(fleet, {log}) avvia tutte le celle
2
+ // Fleet boot companion: bootCells(fleet, {log}) starts the boot:true cells
3
3
  // boot:true di un fleet provider (builtin) chiamandone up(). Una cella gia' in
4
4
  // esecuzione (up -> httpError 409 / "duplicate session") = SKIP non fatale;
5
5
  // ogni altro errore viene raccolto in failed[] e NON ferma le altre celle.
6
- // Nessun side-effect all'import. Design §4c / §9b.
6
+ // No side effects on import.
7
7
  //
8
8
  // Agnostico: usa solo il contratto fleet {available, status(), up()} — non legge
9
9
  // internals delle definizioni. La lista delle celle boot:true arriva da status()