@mmmbuto/nexuscrew 0.9.24 → 0.9.25

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.
@@ -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],
@@ -0,0 +1,142 @@
1
+ 'use strict';
2
+ // Provisioning dell'authority mode. Genera le DUE credenziali distinte
3
+ // (daemon + launcher) nella directory dell'authority, con permessi 0700/0600,
4
+ // e attiva `fleet.identity.mode = "authority"` nella config. Idempotente:
5
+ // credenziali esistenti non vengono MAI sovrascritte senza --force.
6
+ //
7
+ // Nessun valore di credenziale torna nel ritorno e nessun valore finisce nei
8
+ // log: il ritorno descrive solo percorsi e azioni.
9
+
10
+ const fs = require('node:fs');
11
+ const path = require('node:path');
12
+ const crypto = require('node:crypto');
13
+
14
+ const CREDENTIAL_FILES = Object.freeze({
15
+ identityDaemonCredential: 'daemon.credential',
16
+ identityLauncherCredential: 'launcher.credential',
17
+ });
18
+
19
+ // Le due credenziali devono essere DISTINTE (stessa regola di
20
+ // identity-authority a runtime): confronto a tempo costante sui byte.
21
+ function credentialsDistinct(daemonCredential, launcherCredential) {
22
+ if (typeof daemonCredential !== 'string' || !daemonCredential
23
+ || typeof launcherCredential !== 'string' || !launcherCredential) return false;
24
+ const daemon = Buffer.from(daemonCredential);
25
+ const launcher = Buffer.from(launcherCredential);
26
+ if (daemon.length !== launcher.length) return true;
27
+ return !crypto.timingSafeEqual(daemon, launcher);
28
+ }
29
+
30
+ function defaultAuthorityDir(home) {
31
+ return path.join(home || require('node:os').homedir(), '.nexuscrew', 'identity-authority');
32
+ }
33
+
34
+ function assertSafeMode(filePath, expectedMode) {
35
+ const st = fs.lstatSync(filePath);
36
+ if (!st.isFile()) throw new Error(`identity provision: ${filePath} non e' un file regolare`);
37
+ if ((st.mode & 0o777) !== expectedMode) {
38
+ throw new Error(`identity provision: ${filePath} ha permessi ${(st.mode & 0o777).toString(8)} (attesi ${expectedMode.toString(8)})`);
39
+ }
40
+ }
41
+
42
+ function existingCredentials(dir) {
43
+ const found = [];
44
+ for (const name of Object.values(CREDENTIAL_FILES)) {
45
+ const filePath = path.join(dir, name);
46
+ try {
47
+ const st = fs.lstatSync(filePath);
48
+ if (st.isFile() && st.size > 0) found.push(filePath);
49
+ } catch (_) { /* assente: ok */ }
50
+ }
51
+ return found;
52
+ }
53
+
54
+ function provisionIdentityAuthority({
55
+ home,
56
+ dir,
57
+ force = false,
58
+ writeConfig = true,
59
+ configPath,
60
+ randomBytes = crypto.randomBytes,
61
+ fsImpl = fs,
62
+ now = new Date,
63
+ } = {}) {
64
+ const targetDir = dir || defaultAuthorityDir(home);
65
+ fsImpl.mkdirSync(targetDir, { recursive: true, mode: 0o700 });
66
+
67
+ const preExisting = existingCredentials(targetDir);
68
+ if (preExisting.length && !force) {
69
+ return {
70
+ ok: false, code: 'ALREADY_PROVISIONED', dir: targetDir,
71
+ existing: preExisting,
72
+ message: 'identity authority already provisioned; pass --force to regenerate',
73
+ };
74
+ }
75
+
76
+ const written = [];
77
+ let daemonValue = randomBytes(32).toString('hex');
78
+ let launcherValue = randomBytes(32).toString('hex');
79
+ // Rigenera finché le due credenziali sono distinte (confronto a tempo
80
+ // costante, stessa regola di identity-authority). Con una fonte casuale
81
+ // sana la prima iterazione passa; con randomBytes iniettati degeneri il
82
+ // provisioning viene RIFIUTATO anche con --force, senza scrivere nulla.
83
+ for (let attempt = 0; attempt < 8; attempt += 1) {
84
+ if (credentialsDistinct(daemonValue, launcherValue)) break;
85
+ launcherValue = randomBytes(32).toString('hex');
86
+ if (attempt === 7 && !credentialsDistinct(daemonValue, launcherValue)) {
87
+ return {
88
+ ok: false, code: 'IDENTICAL_CREDENTIALS', dir: targetDir,
89
+ message: 'identity provision: generated credentials are identical; nothing written',
90
+ };
91
+ }
92
+ }
93
+ for (const [name, value] of [
94
+ [CREDENTIAL_FILES.identityDaemonCredential, daemonValue],
95
+ [CREDENTIAL_FILES.identityLauncherCredential, launcherValue],
96
+ ]) {
97
+ const filePath = path.join(targetDir, name);
98
+ fsImpl.writeFileSync(filePath, `${value}\n`, { mode: 0o600 });
99
+ written.push(filePath);
100
+ }
101
+ // I permessi espliciti valgono anche su filesystem che ignorano mode in
102
+ // writeFileSync (umask larghe): rinforza dopo la scrittura.
103
+ fsImpl.chmodSync(targetDir, 0o700);
104
+ for (const filePath of written) fsImpl.chmodSync(filePath, 0o600);
105
+ for (const filePath of written) assertSafeMode(filePath, 0o600);
106
+
107
+ const resolvedConfigPath = configPath
108
+ || (process.env.NEXUSCREW_CONFIG_FILE
109
+ || path.join(home || require('node:os').homedir(), '.nexuscrew', 'config.json'));
110
+
111
+ let configUpdated = false;
112
+ let configInstructions = null;
113
+ if (writeConfig) {
114
+ let config = {};
115
+ try {
116
+ config = JSON.parse(fsImpl.readFileSync(resolvedConfigPath, 'utf8'));
117
+ if (!config || typeof config !== 'object' || Array.isArray(config)) config = {};
118
+ } catch (_) { config = {}; }
119
+ config.fleet = { ...(config.fleet || {}), identity: { ...(config.fleet?.identity || {}), mode: 'authority' } };
120
+ fsImpl.mkdirSync(path.dirname(resolvedConfigPath), { recursive: true });
121
+ fsImpl.writeFileSync(resolvedConfigPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
122
+ configUpdated = true;
123
+ } else {
124
+ configInstructions = [
125
+ 'Set the following keys in the NexusCrew config (fleet.identity.mode = "authority")',
126
+ `and keep the two credential files in place under ${targetDir}:`,
127
+ ...Object.values(CREDENTIAL_FILES).map((name) => ` ${path.join(targetDir, name)}`),
128
+ ].join('\n');
129
+ }
130
+
131
+ return {
132
+ ok: true, dir: targetDir, written, configUpdated, configInstructions,
133
+ mode: 'authority',
134
+ };
135
+ }
136
+
137
+ module.exports = {
138
+ provisionIdentityAuthority,
139
+ defaultAuthorityDir,
140
+ credentialsDistinct,
141
+ CREDENTIAL_FILES,
142
+ };
@@ -29,6 +29,14 @@ const OLLAMA_CLOUD_MODELS = Object.freeze([
29
29
  // tools/thinking/vision/cloud; 552B MoE (8B attivi).
30
30
  // DICHIARATO 2026-09-11 https://ollama.com/library/deepseek-v4.1-flash
31
31
  'deepseek-v4.1-flash',
32
+ // Forme nude gemma4 e qwen3.5: MISURATE vive sull'API cloud
33
+ // (/v1/chat/completions -> 200, official measured sample 2026-09-12: 22
34
+ // live / 8 dead variants). Le varianti con tag servite in cloud restano
35
+ // `gemma4:31b` e `qwen3.5:397b`: `gemma4:12b`/`gemma4:e4b` e
36
+ // `qwen3.5:27b`/`:35b`/`:122b` sono 404 (fuori lista di proposito).
37
+ // DICHIARATO 2026-09-12 https://ollama.com/library/gemma4
38
+ // DICHIARATO 2026-09-12 https://ollama.com/library/qwen3.5
39
+ 'gemma4', 'qwen3.5',
32
40
  ]);
33
41
  // Autorita' per OLLAMA_CONTEXT = IL CAMPO STRUTTURATO «Context» della scheda
34
42
  // canale (ollama.com/library, fetch 2026-08-27) — NON la descrizione, che
@@ -63,6 +71,12 @@ const OLLAMA_CONTEXT = Object.freeze({
63
71
  // deepseek-v4.1-flash: scheda «Context / 1M tokens» (2026-09-11); come gli
64
72
  // altri deepseek vale la convenzione binaria del repo (1M -> 1048576).
65
73
  'deepseek-v4.1-flash': 1048576,
74
+ // gemma4 (nudo, tag default `latest`): scheda «128K context window»,
75
+ // Input Text, Image (2026-09-12); convenzione binaria 128K -> 131072.
76
+ 'gemma4': 131072,
77
+ // qwen3.5 (nudo, tag default `latest`): scheda «256K context window»,
78
+ // Input Text, Image (2026-09-12); come la sorella :397b, 256K -> 262144.
79
+ 'qwen3.5': 262144,
66
80
  });
67
81
  // Capacita' per modello OSSERVATE dalla scheda canale (ollama.com/library,
68
82
  // 2026-08-27). Assenza = default conservativo (comportamento odierno, nessuna
@@ -85,6 +99,12 @@ const OLLAMA_MODEL_CAPABILITIES = Object.freeze({
85
99
  // (2026-09-11). PARALLEL NON dichiarato: default conservativo false finche'
86
100
  // non misurato su device.
87
101
  'deepseek-v4.1-flash': Object.freeze({ input: ['text', 'image'], reasoning: true, supportsParallelToolCalls: false }),
102
+ // gemma4: badge vision/tools/thinking/audio/cloud (2026-09-12); scheda Input
103
+ // Text, Image. PARALLEL NON dichiarato: default conservativo false.
104
+ 'gemma4': Object.freeze({ input: ['text', 'image'], reasoning: true, supportsParallelToolCalls: false }),
105
+ // qwen3.5: badge vision/tools/thinking/cloud (2026-09-12); scheda Input
106
+ // Text, Image. PARALLEL NON dichiarato: default conservativo false.
107
+ 'qwen3.5': Object.freeze({ input: ['text', 'image'], reasoning: true, supportsParallelToolCalls: false }),
88
108
  });
89
109
  // Descrittori dell'engine ollama-cloud per la generazione del catalogo client:
90
110
  // stessa forma dei descrittori custom/PI (id, label, contextWindow, input,
@@ -269,20 +289,47 @@ const ALIBABA_PI_MODELS = Object.freeze([
269
289
  // "ha visto l'immagine" da "ha indovinato". Formati immagine per wire:
270
290
  // Messages `source.base64`, Chat `image_url` data URI, Responses
271
291
  // `input_image`.
292
+ //
293
+ // DELTA 2026-09-12 («a segnale»): il piano Go non genera oggi
294
+ // (401 CreditsError su ogni generazione, chiave valida solo sul listing
295
+ // /models -> 200). Le aggiunte qui sotto NON sono misure e2e: sono
296
+ // SIGNAL-ONLY — nessun rifiuto di formato osservato sul gateway
297
+ // (CreditsError = wire accettata, crediti mancati dopo) e id presenti sia
298
+ // sul listing gateway (37) sia nella CLI `opencode models` 1.2.6 (27).
299
+ // eccezione: `qwen3.8-flash` rifiuta Responses con ModelError «format
300
+ // openai» (misurato 2026-09-12): entra SOLO in MESSAGES e CHAT.
301
+ // Rimossi i deprecated `glm-5`, `kimi-k2.5`, `minimax-m2.5`,
302
+ // `qwen3.5-plus`: status=deprecated upstream, assenti dalla CLI 1.2.6
303
+ // (sul gateway restano solo come listing). Storico LIMITS conservato nel
304
+ // commento della tabella.
272
305
  const OPENCODE_GO_MESSAGES_MODELS = Object.freeze([
273
- 'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp', 'deepseek-v4-pro', 'glm-5.2', 'glm-5.1', 'glm-5',
274
- 'minimax-m3', 'minimax-m2.7', 'minimax-m2.5',
275
- 'qwen3.8-max', 'qwen3.7-max', 'qwen3.7-plus', 'qwen3.6-plus', 'qwen3.5-plus',
306
+ // Misurati e2e 2026-08-11 (matrice completa nel commento sopra):
307
+ 'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp', 'deepseek-v4-pro',
308
+ 'glm-5.2', 'glm-5.1', 'minimax-m3', 'minimax-m2.7',
309
+ 'qwen3.8-max', 'qwen3.7-max', 'qwen3.7-plus', 'qwen3.6-plus',
310
+ // Signal-only 2026-09-12 (CreditsError, nessun rifiuto formato):
311
+ 'glm-5.3', 'glm-5.3-flash', 'longcat-2.0', 'deepseek-v4.1-flash',
312
+ 'hy4-preview', 'qwen3.8-flash',
276
313
  ]);
277
314
  const OPENCODE_GO_RESPONSES_MODELS = Object.freeze([
278
- 'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp', 'deepseek-v4-pro', 'gpt-5.6-luna', 'grok-4.6', 'glm-5.2', 'glm-5.1', 'glm-5',
315
+ // Misurati e2e 2026-08-11:
316
+ 'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp', 'deepseek-v4-pro',
317
+ 'gpt-5.6-luna', 'grok-4.6', 'glm-5.2', 'glm-5.1',
318
+ // Signal-only 2026-09-12. qwen3.8-flash ESCLUSO di proposito: Responses
319
+ // rifiutata (ModelError «format openai», misurato 2026-09-12).
320
+ 'glm-5.3', 'glm-5.3-flash', 'longcat-2.0', 'deepseek-v4.1-flash',
321
+ 'hy4-preview',
279
322
  ]);
280
323
  const OPENCODE_GO_CHAT_MODELS = Object.freeze([
281
- 'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp', 'deepseek-v4-pro', 'glm-5.2', 'glm-5.1', 'glm-5',
282
- 'kimi-k3', 'kimi-k2.7-code', 'kimi-k2.6', 'kimi-k2.5',
283
- 'minimax-m3', 'minimax-m2.7', 'minimax-m2.5',
284
- 'qwen3.8-max', 'qwen3.7-max', 'qwen3.7-plus', 'qwen3.6-plus', 'qwen3.5-plus',
285
- 'mimo-v2.5', 'mimo-v2.5-pro', 'hy3', 'gpt-5.6-luna',
324
+ // Misurati e2e 2026-08-11:
325
+ 'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp', 'deepseek-v4-pro',
326
+ 'glm-5.2', 'glm-5.1', 'kimi-k3', 'kimi-k2.7-code', 'kimi-k2.6',
327
+ 'minimax-m3', 'minimax-m2.7', 'qwen3.8-max', 'qwen3.7-max',
328
+ 'qwen3.7-plus', 'qwen3.6-plus', 'mimo-v2.5', 'mimo-v2.5-pro', 'hy3',
329
+ 'gpt-5.6-luna',
330
+ // Signal-only 2026-09-12:
331
+ 'glm-5.3', 'glm-5.3-flash', 'longcat-2.0', 'deepseek-v4.1-flash',
332
+ 'hy4-preview', 'qwen3.8-flash',
286
333
  ]);
287
334
  const OPENCODE_GO_ANTHROPIC_ROOT = 'https://opencode.ai/zen/go';
288
335
  const OPENCODE_GO_API_BASE = 'https://opencode.ai/zen/go/v1';
@@ -296,32 +343,45 @@ const OPENCODE_GO_API_BASE = 'https://opencode.ai/zen/go/v1';
296
343
  //
297
344
  // Restano numeri DICHIARATI, non misurati da noi: se un modello si comporta
298
345
  // come se ne avesse meno, il sospetto va qui prima che sul client.
346
+ // Storico rimosso il 2026-09-12 (id deprecated, fuori CLI 1.2.6): glm-5
347
+ // 202752/32768 · kimi-k2.5 262144/65536 · minimax-m2.5 204800/65536 ·
348
+ // qwen3.5-plus 262144/65536. Rollback: reinserirli qui e nelle tre liste.
299
349
  const OPENCODE_GO_LIMITS = Object.freeze({
300
350
  'deepseek-v4-flash': { context: 1000000, output: 384000 },
301
351
  'deepseek-v4-flash-vision-exp': { context: 1000000, output: 384000 },
302
352
  'deepseek-v4-pro': { context: 1000000, output: 384000 },
303
353
  'glm-5.2': { context: 1000000, output: 131072 },
304
354
  'glm-5.1': { context: 202752, output: 32768 },
305
- 'glm-5': { context: 202752, output: 32768 },
306
355
  'kimi-k3': { context: 1048576, output: 131072 },
307
356
  'kimi-k2.7-code': { context: 262144, output: 262144 },
308
357
  'kimi-k2.6': { context: 262144, output: 65536 },
309
- 'kimi-k2.5': { context: 262144, output: 65536 },
310
358
  'minimax-m3': { context: 1000000, output: 131072 },
311
359
  'minimax-m2.7': { context: 204800, output: 131072 },
312
- 'minimax-m2.5': { context: 204800, output: 65536 },
313
360
  'qwen3.8-max': { context: 1000000, output: 131072 },
314
361
  'qwen3.7-max': { context: 1000000, output: 65536 },
315
362
  'qwen3.7-plus': { context: 1000000, output: 65536 },
316
363
  'qwen3.6-plus': { context: 1000000, output: 65536 },
317
- 'qwen3.5-plus': { context: 262144, output: 65536 },
318
364
  'mimo-v2.5': { context: 1000000, output: 128000 },
319
365
  'mimo-v2.5-pro': { context: 1048576, output: 128000 },
366
+ // hy3: NON allineato a models.dev (che ora dichiara output 128000):
367
+ // fonte divergente, resta il valore trascritto 2026-08-11 finche' non
368
+ // ricontrollato alla fonte. (divergenza segnalata in referto del 2026-09-12)
320
369
  hy3: { context: 256000, output: 64000 },
321
370
  'gpt-5.6-luna': { context: 1050000, output: 128000 },
322
371
  // grok-4.5 -> grok-4.6 (2026-08-27): id aggiornato al modello reale;
323
372
  // limiti confermati da models.dev (xai e opencode-go: 500000/500000).
324
373
  'grok-4.6': { context: 500000, output: 500000 },
374
+ // --- DICHIARATI models.dev 2026-09-12, NON misurati da noi: il piano Go
375
+ // rispondeva 401 CreditsError a ogni generazione il 2026-09-12, quindi
376
+ // nessun numero qui sotto e' stato verificato sul comportamento reale.
377
+ // Wire: nessun rifiuto di formato osservato (signal-only), tranne
378
+ // qwen3.8-flash su Responses (ModelError «format openai», misurato).
379
+ 'deepseek-v4.1-flash': { context: 1000000, output: 384000 },
380
+ 'glm-5.3': { context: 1000000, output: 131072 },
381
+ 'glm-5.3-flash': { context: 1000000, output: 131072 },
382
+ 'longcat-2.0': { context: 1000000, output: 131072 },
383
+ 'qwen3.8-flash': { context: 1000000, output: 131072 },
384
+ 'hy4-preview': { context: 1024000, output: 64000 },
325
385
  });
326
386
 
327
387
  function opencodeGoContextFor(model) {
@@ -1708,6 +1768,13 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
1708
1768
  // env, e i rami di trattamento (che confrontano il nome nuovo) non scattano:
1709
1769
  // la cella parte, sembra a posto, e gira con i parametri sbagliati.
1710
1770
  const env = {}; const args = []; const model = canonicalModel(cell?.model || spec.model);
1771
+ // Identita' della cella protetta (solo codex-vl, l'unico client con un
1772
+ // REQUIRED). La DECISIONE sul canale nasce qui, dove nasce il valore di
1773
+ // CODEX_APP_SERVER_IDENTITY_REQUIRED, e viaggia col payload: se la derivasse
1774
+ // il supervisore (cell-exec aggiungeva sempre fd 3:4) i due potevano
1775
+ // divergere. Divergono, e una cella standalone muore sull'handshake.
1776
+ let identityChannel = null;
1777
+ let identityAuthorityUnavailable = '';
1711
1778
  // V-69: motivo per cui il prompt per-cella di vl NON e' stato composto
1712
1779
  // (versione del runtime che non regge, o file non scrivibile in sicurezza).
1713
1780
  // Stringa vuota = consegnato. Sale con l'engine risolto come vlPromptDegraded.
@@ -2044,6 +2111,24 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
2044
2111
  // as the runtime compatibility override for callers that do not carry a
2045
2112
  // parsed engine definition. Other managed clients never receive this key.
2046
2113
  if (spec.client === 'codex-vl') {
2114
+ // REQUIRED=1 ha senso SOLO quando l'authority mode e' attivo e
2115
+ // l'authority e' costruibile (credenziali presenti). Fuori di quello il
2116
+ // relay non puo' emettere subject: forzare 1 produce daemon che non
2117
+ // partono (fail-closed senza canale) o attach con `revoked` sistematico.
2118
+ const identityMode = cfg.fleetIdentityMode
2119
+ || (cfg.fleet && cfg.fleet.identity && cfg.fleet.identity.mode)
2120
+ || 'legacy';
2121
+ const identityCredentialsDistinct = (a, b) => (a && b
2122
+ ? Buffer.from(String(a)).compare(Buffer.from(String(b))) !== 0
2123
+ : false);
2124
+ const identityAuthorityReady = !!(cfg.identityAuthority
2125
+ || (cfg.identityDaemonCredential && cfg.identityLauncherCredential
2126
+ && identityCredentialsDistinct(cfg.identityDaemonCredential, cfg.identityLauncherCredential)));
2127
+ const identityAuthorityFault = cfg.identityAuthorityFault
2128
+ || (cfg.identityDaemonCredential && cfg.identityLauncherCredential
2129
+ && !identityCredentialsDistinct(cfg.identityDaemonCredential, cfg.identityLauncherCredential)
2130
+ ? 'daemon and launcher credentials are identical' : null);
2131
+ const logLine = typeof cfg.log === 'function' ? cfg.log : null;
2047
2132
  const definitionEnv = engine && engine.env;
2048
2133
  const runtimeEnv = cfg && cfg.env;
2049
2134
  const hasDefinitionOverride = definitionEnv && Object.prototype.hasOwnProperty.call(
@@ -2052,12 +2137,49 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
2052
2137
  const hasRuntimeOverride = runtimeEnv && Object.prototype.hasOwnProperty.call(
2053
2138
  runtimeEnv, CODEX_APP_SERVER_IDENTITY_REQUIRED,
2054
2139
  );
2140
+ const authorityActive = identityMode === 'authority' && identityAuthorityReady;
2141
+ // Authority configurata ma non costruibile (credenziali assenti, identiche
2142
+ // o con permessi sbagliati): la cella protetta NON parte. Esportare '0' con
2143
+ // una riga di log era un degrado silenzioso — il child partiva standalone e
2144
+ // il motivo viveva solo nel log. Il rifiuto lo applica il launcher, col
2145
+ // codice IDENTITY_AUTHORITY_UNAVAILABLE (runtime.js); qui si dichiara il
2146
+ // MOTIVO, senza valori.
2147
+ const authorityUnavailable = identityMode === 'authority' && !identityAuthorityReady
2148
+ ? (identityAuthorityFault || 'authority credentials missing')
2149
+ : '';
2055
2150
  if (hasDefinitionOverride && typeof definitionEnv[CODEX_APP_SERVER_IDENTITY_REQUIRED] === 'string') {
2056
2151
  env[CODEX_APP_SERVER_IDENTITY_REQUIRED] = definitionEnv[CODEX_APP_SERVER_IDENTITY_REQUIRED];
2152
+ identityChannel = env[CODEX_APP_SERVER_IDENTITY_REQUIRED] === '1';
2153
+ if (!authorityActive && identityChannel && logLine) {
2154
+ logLine(`fleet identity: override definition force CODEX_APP_SERVER_IDENTITY_REQUIRED=1 `
2155
+ + `but identity mode is '${identityMode}' without authority credentials`);
2156
+ }
2057
2157
  } else if (hasRuntimeOverride && typeof runtimeEnv[CODEX_APP_SERVER_IDENTITY_REQUIRED] === 'string') {
2058
2158
  env[CODEX_APP_SERVER_IDENTITY_REQUIRED] = runtimeEnv[CODEX_APP_SERVER_IDENTITY_REQUIRED];
2059
- } else {
2159
+ identityChannel = env[CODEX_APP_SERVER_IDENTITY_REQUIRED] === '1';
2160
+ if (!authorityActive && identityChannel && logLine) {
2161
+ logLine(`fleet identity: override runtime force CODEX_APP_SERVER_IDENTITY_REQUIRED=1 `
2162
+ + `but identity mode is '${identityMode}' without authority credentials`);
2163
+ }
2164
+ } else if (authorityActive) {
2060
2165
  env[CODEX_APP_SERVER_IDENTITY_REQUIRED] = CODEX_APP_SERVER_IDENTITY_REQUIRED_DEFAULT;
2166
+ identityChannel = env[CODEX_APP_SERVER_IDENTITY_REQUIRED] === '1';
2167
+ } else if (authorityUnavailable) {
2168
+ // Nessuna esportazione: il lancio e' rifiutato prima dello spawn, quindi
2169
+ // il valore non arriverebbe mai a un child. Un override esplicito (rami
2170
+ // sopra) resta una scelta dell'operatore e non entra qui.
2171
+ identityChannel = false;
2172
+ identityAuthorityUnavailable = authorityUnavailable;
2173
+ if (logLine) {
2174
+ logLine('fleet identity: protected cell launch refused — authority configured but unavailable ('
2175
+ + authorityUnavailable + ')');
2176
+ }
2177
+ } else {
2178
+ env[CODEX_APP_SERVER_IDENTITY_REQUIRED] = '0';
2179
+ identityChannel = false;
2180
+ if (logLine) {
2181
+ logLine(`fleet identity: CODEX_APP_SERVER_IDENTITY_REQUIRED=0 (identity mode '${identityMode}')`);
2182
+ }
2061
2183
  }
2062
2184
  }
2063
2185
  // Prompt su argv (0.8.47): SOLO i client che non hanno un percorso classified
@@ -2106,6 +2228,10 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
2106
2228
  return { ok: true, info, engine: {
2107
2229
  ...engine, command, args, env,
2108
2230
  promptMode: promptViaDelivery ? 'send-keys' : 'managed-argv', clientBinary: info.binary,
2231
+ // Canale identita' (vedi sopra): assente per i client senza REQUIRED, cosi'
2232
+ // il payload resta quello di prima e il supervisore conserva il default.
2233
+ ...(identityChannel === null ? {} : { identityChannel }),
2234
+ ...(identityAuthorityUnavailable ? { identityAuthorityUnavailable } : {}),
2109
2235
  ...(vlPromptDegraded ? { vlPromptDegraded } : {}),
2110
2236
  ...(spec.client === 'shell' ? { shellOneShot } : {}),
2111
2237
  } };
@@ -41,6 +41,22 @@ function findEngine(defs, id) { return defs.engines.find((e) => e.id === id) ||
41
41
  // boot = definizioni iniziali (loadDefinitions, non null: il caller gia'
42
42
  // e' tornato unavailable su garbage).
43
43
  // ---------------------------------------------------------------------------
44
+ // Preflight identita': un'authority configurata ma non costruibile
45
+ // REFUTA il lancio della cella protetta invece di degradarla a standalone in
46
+ // silenzio. Il motivo non contiene valori: e' la stessa stringa che il
47
+ // risolutore (lib/fleet/managed.js) mette su `identityAuthorityUnavailable`.
48
+ // Il codice e' stabile e destinato all'operatore, come gli altri IDENTITY_*.
49
+ function identityLaunchRefusal(launchEngine) {
50
+ const reason = launchEngine && launchEngine.identityAuthorityUnavailable;
51
+ if (!reason) return null;
52
+ return {
53
+ status: 500,
54
+ code: 'IDENTITY_AUTHORITY_UNAVAILABLE',
55
+ message: `fleet identity: authority configurata ma non costruibile (${reason}); `
56
+ + 'la cella protetta non parte',
57
+ };
58
+ }
59
+
44
60
  function createBuiltinRuntime(ctx) {
45
61
  const {
46
62
  cfg, home, defsPath, tmuxBin, readonly, launchBroker, leaseManager, boot, ensureProtection,
@@ -214,6 +230,15 @@ function createBuiltinRuntime(ctx) {
214
230
  const log = typeof cfg.log === 'function' ? cfg.log : console.warn;
215
231
  log(`fleet vl ${cell.id}: ${launchEngine.vlPromptDegraded}`);
216
232
  }
233
+ // L'authority e' configurata ma non e' costruibile — il child non
234
+ // partirebbe con un'identita' verificabile, quindi non parte affatto.
235
+ // Prima si esportava REQUIRED=0 con una riga di log: la cella si avviava
236
+ // standalone e il degrado si scopriva solo leggendo il log.
237
+ const refusal = identityLaunchRefusal(launchEngine);
238
+ if (refusal) {
239
+ throw httpError(refusal.status, refusal.message, null,
240
+ { phase: 'preflight', code: refusal.code });
241
+ }
217
242
  }
218
243
 
219
244
  // (2) trust del command PRIMA di lanciare
@@ -264,6 +289,10 @@ function createBuiltinRuntime(ctx) {
264
289
  NEXUSCREW_MCP_SESSION: cell.tmuxSession,
265
290
  },
266
291
  ...(identity ? { identity } : {}),
292
+ // La decisione sul canale viaggia col payload: il supervisore non la
293
+ // ricalcola (era il punto in cui i due potevano divergere).
294
+ ...(typeof launchEngine.identityChannel === 'boolean'
295
+ ? { identityChannel: launchEngine.identityChannel } : {}),
267
296
  ...(leaseInfo ? { lease: { cellId: cell.id, launchEpoch: leaseInfo.launchEpoch, stablePath: leaseInfo.stablePath } } : {}),
268
297
  supervise: {
269
298
  enabled: !launchEngine.shellOneShot,
@@ -568,4 +597,4 @@ function createBuiltinRuntime(ctx) {
568
597
  };
569
598
  }
570
599
 
571
- module.exports = { createBuiltinRuntime, findCell, findEngine, STATUS_TTL_MS };
600
+ module.exports = { createBuiltinRuntime, findCell, findEngine, identityLaunchRefusal, STATUS_TTL_MS };