@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.
- package/CHANGELOG.md +76 -0
- package/frontend/dist/version.json +1 -1
- package/lib/cli/commands.js +33 -1
- package/lib/cli/doctor.js +87 -4
- package/lib/config.js +51 -1
- package/lib/fleet/builtin.js +13 -1
- package/lib/fleet/catalogs/opencode-go.json +200 -16
- package/lib/fleet/cell-exec.js +31 -5
- package/lib/fleet/identity-provision.js +142 -0
- package/lib/fleet/managed.js +140 -14
- package/lib/fleet/runtime.js +30 -1
- package/lib/mcp/identity-provider.js +133 -5
- package/lib/mcp/identity-schema.js +2 -0
- package/lib/mcp/server.js +50 -9
- package/lib/mcp/tools.js +15 -4
- package/package.json +2 -2
package/lib/fleet/cell-exec.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
+
};
|
package/lib/fleet/managed.js
CHANGED
|
@@ -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
|
-
|
|
274
|
-
'
|
|
275
|
-
'
|
|
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
|
-
|
|
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
|
-
|
|
282
|
-
'
|
|
283
|
-
'
|
|
284
|
-
'
|
|
285
|
-
'mimo-v2.5', 'mimo-v2.5-pro', 'hy3',
|
|
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
|
-
|
|
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
|
} };
|
package/lib/fleet/runtime.js
CHANGED
|
@@ -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 };
|