@mmmbuto/nexuscrew 0.9.24 → 0.9.26

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.
@@ -86,7 +86,7 @@ Lifecycle semantics:
86
86
 
87
87
  const CLI_VALUE_FLAGS = new Set([
88
88
  'label', 'ssh', 'ssh-port', 'autostart', 'visibility', 'selected',
89
- 'name', 'local-name', 'local-label', 'identity-file', 'port',
89
+ 'name', 'local-name', 'local-label', 'identity-file', 'port', 'dir',
90
90
  ]);
91
91
 
92
92
  // valueFlags (Set|array opzionale): flag che consumano il token successivo nella
@@ -1508,6 +1508,38 @@ function dispatch(argv, opts = {}) {
1508
1508
  serve({ ...opts, pidfile: flags.pidfile, serverStart: opts.serverStart });
1509
1509
  return { code: 0, keepAlive: true }; // server.listen tiene il processo vivo; non exit
1510
1510
  }
1511
+ if (cmd === 'identity') {
1512
+ const sub = rest[1];
1513
+ if (sub !== 'provision') {
1514
+ log('usage: nexuscrew identity provision [--force] [--no-write-config] [--dir <path>]');
1515
+ return { code: sub === undefined ? 0 : 1 };
1516
+ }
1517
+ const provision = require('../fleet/identity-provision.js').provisionIdentityAuthority;
1518
+ const home = opts.home || require('node:os').homedir();
1519
+ const result = provision({
1520
+ home,
1521
+ dir: flags.dir,
1522
+ force: flags.force === true,
1523
+ writeConfig: flags['no-write-config'] !== true,
1524
+ });
1525
+ if (!result.ok) {
1526
+ log(`identity provision: ${result.message}`);
1527
+ log(` existing credential files:`);
1528
+ for (const filePath of result.existing) log(` ${filePath}`);
1529
+ return { code: 1 };
1530
+ }
1531
+ log(`identity provision: credentials written under ${result.dir} (0600, dir 0700)`);
1532
+ log('identity provision: fleet.identity.mode = "authority"');
1533
+ if (result.configUpdated) {
1534
+ log(`identity provision: config updated (${result.mode})`);
1535
+ } else {
1536
+ log('identity provision: config NOT written (--no-write-config). Apply manually:');
1537
+ log(' fleet.identity.mode = "authority"');
1538
+ log(` keep both credential files in place under ${result.dir}`);
1539
+ }
1540
+ log('identity provision: restart the NexusCrew service to activate authority mode');
1541
+ return { code: 0 };
1542
+ }
1511
1543
  if (cmd === 'doctor') {
1512
1544
  if (flags.peers === true) return dispatchNodes(['nodes', 'doctor'], flags, opts);
1513
1545
  const r = doctor({ ...opts, log });
package/lib/cli/doctor.js CHANGED
@@ -462,6 +462,24 @@ function checkMcpIdentity(env) {
462
462
  const e = env || process.env;
463
463
  const hasTmux = !!(typeof e.TMUX === 'string' && e.TMUX);
464
464
  const hasSession = !!(typeof e.NEXUSCREW_MCP_SESSION === 'string' && e.NEXUSCREW_MCP_SESSION.trim());
465
+ const verifiedNames = [
466
+ 'NEXUSCREW_VERIFIED_ENV_VERSION',
467
+ 'NEXUSCREW_VERIFIED_OWNER_INSTANCE_ID',
468
+ 'NEXUSCREW_VERIFIED_CELL_ID',
469
+ 'NEXUSCREW_VERIFIED_INCARNATION_ID',
470
+ 'NEXUSCREW_VERIFIED_BINDING_ID',
471
+ 'NEXUSCREW_VERIFIED_ORIGIN',
472
+ 'NEXUSCREW_VERIFIED_THREAD_ID',
473
+ ];
474
+ const verifiedPresent = verifiedNames.filter((name) => typeof e[name] === 'string' && e[name]);
475
+ if (verifiedPresent.length) {
476
+ return {
477
+ name: 'MCP identity env',
478
+ ok: true,
479
+ detail: `metadati verified osservabili (${verifiedPresent.length}/7 nomi NEXUSCREW_VERIFIED_*): `
480
+ + 'identita attribuita solo dopo introspezione authority riuscita',
481
+ };
482
+ }
465
483
  if (hasTmux || hasSession) {
466
484
  const src = [];
467
485
  if (hasTmux) src.push('TMUX');
@@ -469,16 +487,80 @@ function checkMcpIdentity(env) {
469
487
  return {
470
488
  name: 'MCP identity env',
471
489
  ok: true,
472
- detail: `identita osservabile (${src.join('+')})`,
490
+ detail: `identita legacy osservabile (${src.join('+')})`,
473
491
  };
474
492
  }
475
493
  return {
476
494
  name: 'MCP identity env',
477
495
  ok: true, // mai FAIL: PWA-only senza MCP configurato non deve rompere il doctor
478
496
  warn: true,
479
- detail: 'nessuna identita MCP osservabile (TMUX/NEXUSCREW_MCP_SESSION assenti nel processo doctor); '
480
- + 'se integri l\'MCP stdio allowlista i nomi (codex-vl mcp add --env-var) — PWA-only: ignorabile',
497
+ detail: 'nessuna identita MCP osservabile (nessun NEXUSCREW_VERIFIED_* e nessuna variabile legacy '
498
+ + 'nel processo doctor). Percorsi validi: sessione verified (codex-vl >= 0.154.0-vl.3 + NexusCrew '
499
+ + '>= 0.9.25 con authority mode provisionato) oppure sessione legacy con le tre variabili ereditate '
500
+ + 'dal client MCP. NON allowlistare i nomi legacy in env_vars del server MCP: con vl.3 lo spawn '
501
+ + 'verified fallisce — PWA-only: ignorabile',
502
+ };
503
+ }
504
+
505
+ // Stato dell'authority mode (registro flotta). Mostra mode e costruibilita' dell'
506
+ // authority (file presenti, permessi) SENZA mai esporre valori.
507
+ function checkIdentityAuthorityMode({ home, configPath, env } = {}) {
508
+ const e = env || process.env;
509
+ const fs = require('node:fs');
510
+ const path = require('node:path');
511
+ const cfgPath = configPath
512
+ || process.env.NEXUSCREW_CONFIG_FILE
513
+ || path.join(home || os.homedir(), '.nexuscrew', 'config.json');
514
+ let cfg = {};
515
+ try { cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8')) || {}; } catch (_) { cfg = {}; }
516
+ const mode = cfg.fleetIdentityMode
517
+ || (cfg.fleet && cfg.fleet.identity && cfg.fleet.identity.mode)
518
+ || 'legacy';
519
+ const dir = cfg.identityAuthorityDir || path.join(home || os.homedir(), '.nexuscrew', 'identity-authority');
520
+ const files = ['daemon.credential', 'launcher.credential'];
521
+ const present = [];
522
+ const values = {};
523
+ const problems = [];
524
+ for (const name of files) {
525
+ const filePath = path.join(dir, name);
526
+ try {
527
+ const st = fs.lstatSync(filePath);
528
+ if (!st.isFile()) { problems.push(`${name}: non regolare`); continue; }
529
+ if ((st.mode & 0o777) !== 0o600) { problems.push(`${name}: permessi non 0600`); continue; }
530
+ const value = fs.readFileSync(filePath, 'utf8').trim();
531
+ if (!value) { problems.push(`${name}: vuoto`); continue; }
532
+ present.push(name);
533
+ values[name] = value;
534
+ } catch (_) { problems.push(`${name}: assente`); }
535
+ }
536
+ // The two credentials must be DISTINCT (constant-time comparison):
537
+ // identical means the authority is not constructible, fail-closed.
538
+ let distinctLine = 'credentials distinct: n/a (una o entrambe assenti)';
539
+ if (present.length === 2) {
540
+ const distinct = Buffer.from(values['daemon.credential'])
541
+ .compare(Buffer.from(values['launcher.credential'])) !== 0;
542
+ distinctLine = `credentials distinct: ${distinct ? 'yes' : 'no'}`;
543
+ if (!distinct) problems.push('credenziali daemon e launcher identiche');
544
+ }
545
+ const constructible = present.length === 2 && problems.length === 0;
546
+ const ok = mode !== 'authority' || constructible;
547
+ const out = {
548
+ name: 'Fleet identity authority mode',
549
+ ok,
550
+ detail: `mode=${mode}; credenziali: ${present.length === 2 ? 'entrambe presenti (0600)' : present.join('+') || 'nessuna'} `
551
+ + `(${dir}); ${distinctLine}${problems.length ? '; problemi: ' + problems.join(', ') : ''}`,
481
552
  };
553
+ if (mode !== 'authority') {
554
+ out.warn = true;
555
+ out.detail += ' — il launcher espone CODEX_APP_SERVER_IDENTITY_REQUIRED=0: provisiona con '
556
+ + '`nexuscrew identity provision` per attivare l\'authority mode';
557
+ } else if (!constructible) {
558
+ out.detail += ' — authority: configured but unavailable: '
559
+ + (problems.length ? problems.join(', ') : 'credentials not constructible')
560
+ + ' — le celle protette (codex-vl) non partono [IDENTITY_AUTHORITY_UNAVAILABLE]';
561
+ }
562
+ void e;
563
+ return out;
482
564
  }
483
565
 
484
566
  function checkFleetDefinitions(home, fleetDefsPath, enabled = true) {
@@ -578,6 +660,7 @@ function doctor(opts = {}) {
578
660
  checkAutossh(existsImpl),
579
661
  checkSshPermitlisten(opts.sshVersion),
580
662
  checkMcpIdentity(opts.env),
663
+ checkIdentityAuthorityMode({ home: opts.home, env: opts.env }),
581
664
  checkTmuxServerCwd(platform, execImpl, { tmuxBin: opts.tmuxBin, procCwdImpl: opts.procCwdImpl }),
582
665
  checkTmuxServerTermuxPreload(opts.env, execImpl, { platform, home, tmuxBin: opts.tmuxBin }),
583
666
  ];
@@ -601,6 +684,6 @@ module.exports = {
601
684
  checkFleetServiceWorkingDirectory,
602
685
  checkTmuxSurvival, checkUserLinger,
603
686
  checkSshClient, checkAutossh, checkSshPermitlisten,
604
- checkMcpIdentity, checkTermuxExec,
687
+ checkMcpIdentity, checkIdentityAuthorityMode, checkTermuxExec,
605
688
  checkTmuxServerCwd, checkTmuxServerTermuxPreload,
606
689
  };
package/lib/config.js CHANGED
@@ -135,8 +135,58 @@ function envOverrides() {
135
135
  }
136
136
 
137
137
  // Precedence: baseDefaults < config.json < env < opts. [B2]
138
+ // Le credenziali authority vivono in due file 0600 nella dir
139
+ // dell'authority (default ~/.nexuscrew/identity-authority). Se esistono vengono
140
+ // caricate qui, senza MAI loggare i valori: builtin costruisce l'authority
141
+ // solo quando ENTRAMBE sono presenti e distinte.
142
+ function readIdentityAuthorityCredentials(cfg = {}) {
143
+ const fs = require('node:fs');
144
+ const crypto = require('node:crypto');
145
+ const dir = cfg.identityAuthorityDir
146
+ || path.join(cfg.home || os.homedir(), '.nexuscrew', 'identity-authority');
147
+ // Enforced AT LOAD. Identical credentials or wrong permissions
148
+ // (files != 0600, dir != 0700) => the credentials are NOT loaded: the
149
+ // authority is not constructed and the protected launch is refused instead.
150
+ // The reason travels in identityAuthorityFault, and never carries a value.
151
+ const faults = [];
152
+ let dirMode = null;
153
+ try { dirMode = fs.lstatSync(dir).mode & 0o777; } catch (_) { faults.push('authority dir missing'); }
154
+ if (dirMode !== null && dirMode !== 0o700) faults.push('authority dir mode is not 0700');
155
+ const readOne = (name) => {
156
+ try {
157
+ const st = fs.lstatSync(path.join(dir, name));
158
+ if (!st.isFile()) { faults.push(`${name}: not a regular file`); return undefined; }
159
+ if ((st.mode & 0o777) !== 0o600) { faults.push(`${name}: mode is not 0600`); return undefined; }
160
+ const value = fs.readFileSync(path.join(dir, name), 'utf8').trim();
161
+ if (!value) { faults.push(`${name}: empty`); return undefined; }
162
+ return value;
163
+ } catch (_) { faults.push(`${name}: missing`); return undefined; }
164
+ };
165
+ const daemon = readOne('daemon.credential');
166
+ const launcher = readOne('launcher.credential');
167
+ if (daemon && launcher) {
168
+ let distinct;
169
+ try {
170
+ distinct = !crypto.timingSafeEqual(Buffer.from(daemon), Buffer.from(launcher));
171
+ } catch (_) { distinct = false; }
172
+ if (!distinct) faults.push('daemon and launcher credentials are identical');
173
+ }
174
+ const out = {};
175
+ if (faults.length) {
176
+ out.identityAuthorityFault = faults.join('; ');
177
+ return out;
178
+ }
179
+ if (daemon) out.identityDaemonCredential = daemon;
180
+ if (launcher) out.identityLauncherCredential = launcher;
181
+ return out;
182
+ }
183
+
138
184
  function loadConfig(opts = {}) {
139
- return { ...baseDefaults(), ...readConfigJson(), ...envOverrides(), ...opts };
185
+ const merged = { ...baseDefaults(), ...readConfigJson(), ...envOverrides(), ...opts };
186
+ // I file di credenziale sono fallback: non sovrascrivono mai valori espliciti
187
+ // di config.json/env/opts (precedence invariata).
188
+ const creds = readIdentityAuthorityCredentials(merged);
189
+ return { ...merged, ...creds };
140
190
  }
141
191
 
142
192
  // Retrocompat: defaults() = baseDefaults + env (NO config.json — per test isolati
@@ -700,11 +700,23 @@ async function createBuiltinFleet(cfg = {}) {
700
700
  // leaseManager e launchBroker. L'authority resta server-side: nessuna
701
701
  // credenziale o chiave HMAC attraversa il payload consegnato alla cella.
702
702
  const identityMode = cfg.fleetIdentityMode || cfg.fleet?.identity?.mode || 'legacy';
703
+ // Identical credentials => the authority is NOT constructed (consistent with
704
+ // identity-authority, which would reject at runtime anyway), and the reason
705
+ // goes to the log.
706
+ const identityCredentialsDistinct = (typeof cfg.identityDaemonCredential === 'string'
707
+ && typeof cfg.identityLauncherCredential === 'string')
708
+ ? Buffer.from(cfg.identityDaemonCredential).compare(Buffer.from(cfg.identityLauncherCredential)) !== 0
709
+ : !!(cfg.identityDaemonCredential && cfg.identityLauncherCredential);
710
+ if (identityMode === 'authority' && !identityCredentialsDistinct && cfg.log) {
711
+ cfg.log('fleet identity: authority mode requested but daemon and launcher credentials '
712
+ + 'are identical or missing; identity enforcement stays off (REQUIRED=0)');
713
+ }
703
714
  const identityOwnerInstanceId = typeof cfg.identityOwnerInstanceId === 'function'
704
715
  ? cfg.identityOwnerInstanceId()
705
716
  : (cfg.identityOwnerInstanceId || null);
706
717
  const identityAuthority = cfg.identityAuthority || (
707
- cfg.identityDaemonCredential && cfg.identityLauncherCredential
718
+ identityCredentialsDistinct
719
+ && cfg.identityDaemonCredential && cfg.identityLauncherCredential
708
720
  ? createIdentityAuthority({
709
721
  dir: cfg.identityAuthorityDir || path.join(home, '.nexuscrew', 'identity-authority'),
710
722
  daemonCredential: cfg.identityDaemonCredential,
@@ -279,9 +279,102 @@
279
279
  "supports_search_tool": false
280
280
  },
281
281
  {
282
- "slug": "glm-5",
283
- "display_name": "glm-5",
284
- "description": "OpenCode Go: glm-5",
282
+ "slug": "deepseek-v4-flash-vision-exp",
283
+ "display_name": "deepseek-v4-flash-vision-exp",
284
+ "description": "OpenCode Go: deepseek-v4-flash-vision-exp (vision)",
285
+ "default_reasoning_level": "high",
286
+ "supported_reasoning_levels": [
287
+ {
288
+ "effort": "low",
289
+ "description": "Fast responses with lighter reasoning"
290
+ },
291
+ {
292
+ "effort": "high",
293
+ "description": "Greater reasoning depth for complex problems"
294
+ },
295
+ {
296
+ "effort": "max",
297
+ "description": "Maximum reasoning depth"
298
+ }
299
+ ],
300
+ "shell_type": "default",
301
+ "visibility": "list",
302
+ "supported_in_api": true,
303
+ "priority": 57,
304
+ "availability_nux": null,
305
+ "upgrade": null,
306
+ "base_instructions": "",
307
+ "supports_reasoning_summaries": true,
308
+ "default_reasoning_summary": "none",
309
+ "support_verbosity": false,
310
+ "default_verbosity": null,
311
+ "apply_patch_tool_type": null,
312
+ "web_search_tool_type": "text",
313
+ "truncation_policy": {
314
+ "mode": "tokens",
315
+ "limit": 1000000
316
+ },
317
+ "supports_parallel_tool_calls": false,
318
+ "supports_image_detail_original": true,
319
+ "context_window": 1000000,
320
+ "effective_context_window_percent": 95,
321
+ "experimental_supported_tools": [],
322
+ "input_modalities": [
323
+ "text",
324
+ "image"
325
+ ],
326
+ "supports_search_tool": false
327
+ },
328
+ {
329
+ "slug": "deepseek-v4.1-flash",
330
+ "display_name": "deepseek-v4.1-flash",
331
+ "description": "OpenCode Go: deepseek-v4.1-flash",
332
+ "default_reasoning_level": "high",
333
+ "supported_reasoning_levels": [
334
+ {
335
+ "effort": "low",
336
+ "description": "Fast responses with lighter reasoning"
337
+ },
338
+ {
339
+ "effort": "high",
340
+ "description": "Greater reasoning depth for complex problems"
341
+ },
342
+ {
343
+ "effort": "max",
344
+ "description": "Maximum reasoning depth"
345
+ }
346
+ ],
347
+ "shell_type": "default",
348
+ "visibility": "list",
349
+ "supported_in_api": true,
350
+ "priority": 58,
351
+ "availability_nux": null,
352
+ "upgrade": null,
353
+ "base_instructions": "",
354
+ "supports_reasoning_summaries": true,
355
+ "default_reasoning_summary": "none",
356
+ "support_verbosity": false,
357
+ "default_verbosity": null,
358
+ "apply_patch_tool_type": null,
359
+ "web_search_tool_type": "text",
360
+ "truncation_policy": {
361
+ "mode": "tokens",
362
+ "limit": 1000000
363
+ },
364
+ "supports_parallel_tool_calls": false,
365
+ "supports_image_detail_original": false,
366
+ "context_window": 1000000,
367
+ "effective_context_window_percent": 95,
368
+ "experimental_supported_tools": [],
369
+ "input_modalities": [
370
+ "text"
371
+ ],
372
+ "supports_search_tool": false
373
+ },
374
+ {
375
+ "slug": "glm-5.3",
376
+ "display_name": "glm-5.3",
377
+ "description": "OpenCode Go: glm-5.3",
285
378
  "default_reasoning_level": "medium",
286
379
  "supported_reasoning_levels": [
287
380
  {
@@ -300,7 +393,7 @@
300
393
  "shell_type": "default",
301
394
  "visibility": "list",
302
395
  "supported_in_api": true,
303
- "priority": 55,
396
+ "priority": 59,
304
397
  "availability_nux": null,
305
398
  "upgrade": null,
306
399
  "base_instructions": "",
@@ -312,11 +405,11 @@
312
405
  "web_search_tool_type": "text",
313
406
  "truncation_policy": {
314
407
  "mode": "tokens",
315
- "limit": 202752
408
+ "limit": 1000000
316
409
  },
317
410
  "supports_parallel_tool_calls": false,
318
411
  "supports_image_detail_original": false,
319
- "context_window": 202752,
412
+ "context_window": 1000000,
320
413
  "effective_context_window_percent": 95,
321
414
  "experimental_supported_tools": [],
322
415
  "input_modalities": [
@@ -325,28 +418,74 @@
325
418
  "supports_search_tool": false
326
419
  },
327
420
  {
328
- "slug": "deepseek-v4-flash-vision-exp",
329
- "display_name": "deepseek-v4-flash-vision-exp",
330
- "description": "OpenCode Go: deepseek-v4-flash-vision-exp (vision)",
331
- "default_reasoning_level": "high",
421
+ "slug": "glm-5.3-flash",
422
+ "display_name": "glm-5.3-flash",
423
+ "description": "OpenCode Go: glm-5.3-flash",
424
+ "default_reasoning_level": "medium",
332
425
  "supported_reasoning_levels": [
333
426
  {
334
427
  "effort": "low",
335
428
  "description": "Fast responses with lighter reasoning"
336
429
  },
430
+ {
431
+ "effort": "medium",
432
+ "description": "Balanced reasoning depth"
433
+ },
337
434
  {
338
435
  "effort": "high",
339
436
  "description": "Greater reasoning depth for complex problems"
437
+ }
438
+ ],
439
+ "shell_type": "default",
440
+ "visibility": "list",
441
+ "supported_in_api": true,
442
+ "priority": 60,
443
+ "availability_nux": null,
444
+ "upgrade": null,
445
+ "base_instructions": "",
446
+ "supports_reasoning_summaries": true,
447
+ "default_reasoning_summary": "none",
448
+ "support_verbosity": false,
449
+ "default_verbosity": null,
450
+ "apply_patch_tool_type": null,
451
+ "web_search_tool_type": "text",
452
+ "truncation_policy": {
453
+ "mode": "tokens",
454
+ "limit": 1000000
455
+ },
456
+ "supports_parallel_tool_calls": false,
457
+ "supports_image_detail_original": false,
458
+ "context_window": 1000000,
459
+ "effective_context_window_percent": 95,
460
+ "experimental_supported_tools": [],
461
+ "input_modalities": [
462
+ "text"
463
+ ],
464
+ "supports_search_tool": false
465
+ },
466
+ {
467
+ "slug": "longcat-2.0",
468
+ "display_name": "longcat-2.0",
469
+ "description": "OpenCode Go: longcat-2.0",
470
+ "default_reasoning_level": "medium",
471
+ "supported_reasoning_levels": [
472
+ {
473
+ "effort": "low",
474
+ "description": "Fast responses with lighter reasoning"
340
475
  },
341
476
  {
342
- "effort": "max",
343
- "description": "Maximum reasoning depth"
477
+ "effort": "medium",
478
+ "description": "Balanced reasoning depth"
479
+ },
480
+ {
481
+ "effort": "high",
482
+ "description": "Greater reasoning depth for complex problems"
344
483
  }
345
484
  ],
346
485
  "shell_type": "default",
347
486
  "visibility": "list",
348
487
  "supported_in_api": true,
349
- "priority": 57,
488
+ "priority": 61,
350
489
  "availability_nux": null,
351
490
  "upgrade": null,
352
491
  "base_instructions": "",
@@ -361,13 +500,58 @@
361
500
  "limit": 1000000
362
501
  },
363
502
  "supports_parallel_tool_calls": false,
364
- "supports_image_detail_original": true,
503
+ "supports_image_detail_original": false,
365
504
  "context_window": 1000000,
366
505
  "effective_context_window_percent": 95,
367
506
  "experimental_supported_tools": [],
368
507
  "input_modalities": [
369
- "text",
370
- "image"
508
+ "text"
509
+ ],
510
+ "supports_search_tool": false
511
+ },
512
+ {
513
+ "slug": "hy4-preview",
514
+ "display_name": "hy4-preview",
515
+ "description": "OpenCode Go: hy4-preview",
516
+ "default_reasoning_level": "medium",
517
+ "supported_reasoning_levels": [
518
+ {
519
+ "effort": "low",
520
+ "description": "Fast responses with lighter reasoning"
521
+ },
522
+ {
523
+ "effort": "medium",
524
+ "description": "Balanced reasoning depth"
525
+ },
526
+ {
527
+ "effort": "high",
528
+ "description": "Greater reasoning depth for complex problems"
529
+ }
530
+ ],
531
+ "shell_type": "default",
532
+ "visibility": "list",
533
+ "supported_in_api": true,
534
+ "priority": 62,
535
+ "availability_nux": null,
536
+ "upgrade": null,
537
+ "base_instructions": "",
538
+ "supports_reasoning_summaries": true,
539
+ "default_reasoning_summary": "none",
540
+ "support_verbosity": false,
541
+ "default_verbosity": null,
542
+ "apply_patch_tool_type": null,
543
+ "web_search_tool_type": "text",
544
+ "truncation_policy": {
545
+ "mode": "tokens",
546
+ "limit": 1024000
547
+ },
548
+ "supports_parallel_tool_calls": false,
549
+ "supports_image_detail_original": false,
550
+ "context_window": 1024000,
551
+ "effective_context_window_percent": 95,
552
+ "experimental_supported_tools": [],
553
+ "input_modalities": [
554
+ "text"
371
555
  ],
372
556
  "supports_search_tool": false
373
557
  }
@@ -111,9 +111,14 @@ function validLease(value) {
111
111
 
112
112
  function validPayload(payload) {
113
113
  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return false;
114
- if (Object.keys(payload).some((key) => !['command', 'args', 'env', 'supervise', 'restartPrompt', 'lease', 'identity'].includes(key))) return false;
114
+ if (Object.keys(payload).some((key) => !['command', 'args', 'env', 'supervise', 'restartPrompt', 'lease', 'identity', 'identityChannel'].includes(key))) return false;
115
115
  if (typeof payload.command !== 'string' || !payload.command || !Array.isArray(payload.args)) return false;
116
116
  if (!payload.env || typeof payload.env !== 'object' || Array.isArray(payload.env)) return false;
117
+ // Il launcher decide se il canale identita' esiste (managed.js) e lo dichiara
118
+ // nel payload con `identityChannel`: il supervisore non lo ricalcola. La chiave
119
+ // va quindi ammessa qui, e SOLO come booleano: un valore di altro tipo e' un
120
+ // payload malformato, non un "canale assente" (assente = caller precedente).
121
+ if (payload.identityChannel !== undefined && typeof payload.identityChannel !== 'boolean') return false;
117
122
  return payload.args.every((v) => typeof v === 'string')
118
123
  && Object.entries(payload.env).every(([k, v]) => /^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(k) && typeof v === 'string')
119
124
  && validSupervise(payload.supervise)
@@ -522,7 +527,16 @@ async function main(argv = process.argv.slice(2), seams = {}) {
522
527
  const sleep = seams.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
523
528
  const proc = seams.process || process;
524
529
  const writeError = seams.writeError || seams.stderrWrite || ((message) => process.stderr.write(message));
525
- const childEnv = { ...payload.env, NEXUSCREW_IDENTITY_FD: '3:4' };
530
+ // Il canale identita' nasce SOLO se il launcher lo ha deciso: la stessa
531
+ // risoluzione che decide CODEX_APP_SERVER_IDENTITY_REQUIRED in managed.js
532
+ // viaggia nel payload come `identityChannel`. Derivarlo qui (aggiungere
533
+ // sempre fd 3:4) permetteva ai due di divergere: una cella standalone
534
+ // riceveva il canale, avviava l'handshake contro un lease server legacy che
535
+ // rispondeva 'revoked', e moriva. Un payload senza il campo (caller
536
+ // precedenti) conserva il comportamento di prima.
537
+ const identityChannel = payload.identityChannel !== false;
538
+ const childEnv = { ...payload.env };
539
+ if (identityChannel) childEnv.NEXUSCREW_IDENTITY_FD = '3:4';
526
540
  // tmux injects these only after the broker ticket was created. Preserve them
527
541
  // for the actual TUI and bind NexusCrew MCP callbacks to the owning session.
528
542
  if (process.env.TMUX) childEnv.TMUX = process.env.TMUX;
@@ -612,7 +626,11 @@ async function main(argv = process.argv.slice(2), seams = {}) {
612
626
  const promptCtl = startGenerationPrompt(payload.restartPrompt, generation, childState, seams);
613
627
  current = spawnImpl(payload.command, payload.args, {
614
628
  env: childEnv,
615
- stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'pipe'],
629
+ // Senza canale non si creano nemmeno le pipe: fd 3 e 4 chiusi nel
630
+ // child (non pipe aperte e inutilizzate).
631
+ stdio: identityChannel
632
+ ? ['inherit', 'inherit', 'inherit', 'pipe', 'pipe']
633
+ : ['inherit', 'inherit', 'inherit'],
616
634
  });
617
635
  // R1a (fix5): exit/error del figlio contano SUBITO, prima di qualunque
618
636
  // await — se il figlio esce durante l'handshake di generazione, l'evento
@@ -649,7 +667,15 @@ async function main(argv = process.argv.slice(2), seams = {}) {
649
667
  ),
650
668
  childExit.then(() => 'exited'),
651
669
  ]);
652
- if (raced === 'ok' && !childSettled) {
670
+ if (!identityChannel) {
671
+ // Nessun canale da aprire: la transizione di generazione resta
672
+ // annunciata al lease (bookkeeping), ma l'identita' non esiste e
673
+ // non c'e' nessun fd da chiudere. L'uscita del figlio resta
674
+ // diagnostica.
675
+ if (raced === 'exited') {
676
+ writeError(`nexuscrew cell: figlio uscito durante l'handshake di generazione (gen ${generation})\n`);
677
+ }
678
+ } else if (raced === 'ok' && !childSettled) {
653
679
  identityCtl = createIdentityChannel({
654
680
  request: current && current.stdio && current.stdio[3],
655
681
  response: current && current.stdio && current.stdio[4],
@@ -671,7 +697,7 @@ async function main(argv = process.argv.slice(2), seams = {}) {
671
697
  } else {
672
698
  writeError(`nexuscrew cell: generazione ${generation} senza identita' (${raced})\n`);
673
699
  }
674
- } else {
700
+ } else if (identityChannel) {
675
701
  identityCtl = createIdentityChannel({
676
702
  request: current && current.stdio && current.stdio[3],
677
703
  response: current && current.stdio && current.stdio[4],