@mmmbuto/nexuscrew 0.8.57 → 0.9.0
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 +164 -2
- package/README.md +1 -0
- package/frontend/dist/assets/index-0vuhL1YP.css +32 -0
- package/frontend/dist/assets/index-zjL6kZ7J.js +93 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/audio/adapters.js +50 -6
- package/lib/cli/commands.js +40 -2
- package/lib/cli/doctor.js +95 -12
- package/lib/cli/init.js +25 -3
- package/lib/cli/path.js +43 -10
- package/lib/cli/pidfile.js +23 -2
- package/lib/config.js +15 -0
- package/lib/fleet/builtin.js +161 -19
- package/lib/fleet/catalogs/opencode-go.json +328 -0
- package/lib/fleet/cell-exec.js +87 -9
- package/lib/fleet/cell-lease-server.js +719 -0
- package/lib/fleet/cell-lease.js +112 -0
- package/lib/fleet/definitions.js +101 -7
- package/lib/fleet/launch-broker.js +115 -3
- package/lib/fleet/lease-client.js +191 -0
- package/lib/fleet/lease-routes.js +92 -0
- package/lib/fleet/lease-verifier.js +230 -0
- package/lib/fleet/managed.js +444 -55
- package/lib/fleet/prompt-delivery.js +50 -2
- package/lib/fleet/provider.js +1 -1
- package/lib/fleet/runtime.js +53 -6
- package/lib/live-host/bridge.js +369 -0
- package/lib/live-host/routes.js +184 -0
- package/lib/live-host/store.js +96 -0
- package/lib/mcp/tools.js +51 -0
- package/lib/nodes/commands.js +9 -2
- package/lib/nodes/store.js +14 -0
- package/lib/nodes/tunnel.js +4 -1
- package/lib/proxy/federation.js +106 -9
- package/lib/proxy/node-proxy.js +33 -0
- package/lib/proxy/panel-auth.js +307 -0
- package/lib/proxy/panel-proxy.js +305 -0
- package/lib/server.js +127 -4
- package/package.json +1 -1
- package/skills/alibaba-token-media/SKILL.md +19 -0
- package/skills/crew/SKILL.md +15 -0
- package/skills/fill-forms/SKILL.md +23 -0
- package/skills/mail-assistant/SKILL.md +15 -0
- package/skills/memory/SKILL.md +15 -0
- package/skills/nexuscrew-agent/SKILL.md +18 -0
- package/skills/vl-msa/SKILL.md +15 -0
- package/frontend/dist/assets/index-CYi_lhCg.css +0 -32
- package/frontend/dist/assets/index-_c-1_3iR.js +0 -93
package/lib/fleet/managed.js
CHANGED
|
@@ -57,16 +57,33 @@ function declaredFor(extraModels, profileId, model) {
|
|
|
57
57
|
// chiamanti perche' ognuno di loro la ricostruirebbe a modo suo, e basta che
|
|
58
58
|
// uno la dimentichi perche' un modello dichiarato smetta di essere valido
|
|
59
59
|
// proprio nel punto che conta — l'avvio.
|
|
60
|
+
//
|
|
61
|
+
// D2: Map<engine, Map<id, model>> — porta il descrittore intero, non solo
|
|
62
|
+
// l'id (stesso motivo del commento gemello in definitions.js: extraModels e'
|
|
63
|
+
// costruita due volte, una dentro parseDefinitions per il parsing, una qui per
|
|
64
|
+
// il runtime che rilegge le definizioni gia' salvate; le due DEVONO restare
|
|
65
|
+
// nella stessa forma).
|
|
60
66
|
function extraModelsFrom(defs) {
|
|
61
67
|
const map = new Map();
|
|
62
68
|
for (const m of (defs && Array.isArray(defs.models) ? defs.models : [])) {
|
|
63
69
|
if (!m || typeof m.engine !== 'string' || typeof m.id !== 'string') continue;
|
|
64
|
-
if (!map.has(m.engine)) map.set(m.engine, new
|
|
65
|
-
map.get(m.engine).
|
|
70
|
+
if (!map.has(m.engine)) map.set(m.engine, new Map());
|
|
71
|
+
map.get(m.engine).set(m.id, m);
|
|
66
72
|
}
|
|
67
73
|
return map;
|
|
68
74
|
}
|
|
69
75
|
|
|
76
|
+
// I descrittori dichiarati per UN profilo (client.provider, es.
|
|
77
|
+
// 'codex-vl.custom'), come array. Chi ha bisogno solo del catalogo dei
|
|
78
|
+
// modelli (customCatalogFor, writePiProviderExtension) chiama questa; chi ha
|
|
79
|
+
// bisogno solo di validare un id (declaredFor) continua a usare extraModels
|
|
80
|
+
// direttamente — due bisogni diversi sulla STESSA struttura, non due copie.
|
|
81
|
+
function declaredModelsFor(extraModels, profileId) {
|
|
82
|
+
if (!extraModels || typeof extraModels.get !== 'function') return [];
|
|
83
|
+
const byId = extraModels.get(profileId);
|
|
84
|
+
return byId && typeof byId.values === 'function' ? [...byId.values()] : [];
|
|
85
|
+
}
|
|
86
|
+
|
|
70
87
|
function canonicalModel(model) {
|
|
71
88
|
const key = String(model || '');
|
|
72
89
|
return Object.hasOwn(MODEL_ALIASES, key) ? MODEL_ALIASES[key] : model;
|
|
@@ -120,22 +137,28 @@ const ALIBABA_PI_MODELS = Object.freeze([
|
|
|
120
137
|
// per distinguere il rifiuto di wire dal payload perso in traduzione).
|
|
121
138
|
//
|
|
122
139
|
// Cosa NON e' in elenco, e perche' non e' una svista:
|
|
123
|
-
// - kimi-*,
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
140
|
+
// - kimi-*, mimo-v2.5*, hy3 fuori da Messages/Responses: il gateway inoltra
|
|
141
|
+
// un payload vuoto e l'upstream risponde "messages must not be empty". E' un
|
|
142
|
+
// difetto loro, reversibile senza preavviso: se un giorno rispondono, l'id
|
|
143
|
+
// si dichiara per quell'engine senza toccare il codice.
|
|
144
|
+
// - deepseek-v4-pro: MISURATO 2026-08-13 sul gateway opencode.ai/zen/go ->
|
|
145
|
+
// 200 su /v1/responses (status=completed) e 200 su /v1/messages
|
|
146
|
+
// (stop_reason=end_turn), auth x-api-key. DeepSeek ha aggiunto la Responses
|
|
147
|
+
// API a v4-pro il 13/08 (l'11/08 era escluso: "messages must not be empty"):
|
|
148
|
+
// per questo ora entra in MESSAGES e RESPONSES. Se smettesse di rispondere,
|
|
149
|
+
// andrebbe rimosso di nuovo — e questo commento va tenuto allineato al codice.
|
|
127
150
|
// - mimo-v2-pro e mimo-v2-omni: deprecati dall'upstream ("migrate to
|
|
128
151
|
// xiaomi/mimo-v2.5*"). hy3-preview: "Model is unavailable".
|
|
129
152
|
// Il catalogo live li pubblicizza comunque; qui non entrano.
|
|
130
153
|
// - grok-4.5 solo su Responses: su Chat risponde 503 e Messages lo rifiuta
|
|
131
154
|
// esplicitamente ("not supported for format anthropic").
|
|
132
155
|
const OPENCODE_GO_MESSAGES_MODELS = Object.freeze([
|
|
133
|
-
'deepseek-v4-flash', 'glm-5.2', 'glm-5.1', 'glm-5',
|
|
156
|
+
'deepseek-v4-flash', 'deepseek-v4-pro', 'glm-5.2', 'glm-5.1', 'glm-5',
|
|
134
157
|
'minimax-m3', 'minimax-m2.7', 'minimax-m2.5',
|
|
135
158
|
'qwen3.8-max', 'qwen3.7-max', 'qwen3.7-plus', 'qwen3.6-plus', 'qwen3.5-plus',
|
|
136
159
|
]);
|
|
137
160
|
const OPENCODE_GO_RESPONSES_MODELS = Object.freeze([
|
|
138
|
-
'deepseek-v4-flash', 'gpt-5.6-luna', 'grok-4.5', 'glm-5.2', 'glm-5.1', 'glm-5',
|
|
161
|
+
'deepseek-v4-flash', 'deepseek-v4-pro', 'gpt-5.6-luna', 'grok-4.5', 'glm-5.2', 'glm-5.1', 'glm-5',
|
|
139
162
|
]);
|
|
140
163
|
const OPENCODE_GO_CHAT_MODELS = Object.freeze([
|
|
141
164
|
'deepseek-v4-flash', 'deepseek-v4-pro', 'glm-5.2', 'glm-5.1', 'glm-5',
|
|
@@ -147,12 +170,85 @@ const OPENCODE_GO_CHAT_MODELS = Object.freeze([
|
|
|
147
170
|
const OPENCODE_GO_ANTHROPIC_ROOT = 'https://opencode.ai/zen/go';
|
|
148
171
|
const OPENCODE_GO_API_BASE = 'https://opencode.ai/zen/go/v1';
|
|
149
172
|
|
|
173
|
+
// Limiti dichiarati dal catalogo models.dev per il provider `opencode-go`,
|
|
174
|
+
// trascritti il 2026-08-11. Sono l'unica fonte autorevole che abbiamo per il
|
|
175
|
+
// contesto: il preflight misura quali coppie wire/modello rispondono, non
|
|
176
|
+
// quanto contesto reggono. Senza questi numeri il client sceglie un default
|
|
177
|
+
// suo — per Codex il catalogo e' proprio cio' che glielo dice, e per Claude
|
|
178
|
+
// l'assenza significa compattare a una soglia che non c'entra col modello.
|
|
179
|
+
//
|
|
180
|
+
// Restano numeri DICHIARATI, non misurati da noi: se un modello si comporta
|
|
181
|
+
// come se ne avesse meno, il sospetto va qui prima che sul client.
|
|
182
|
+
const OPENCODE_GO_LIMITS = Object.freeze({
|
|
183
|
+
'deepseek-v4-flash': { context: 1000000, output: 384000 },
|
|
184
|
+
'deepseek-v4-pro': { context: 1000000, output: 384000 },
|
|
185
|
+
'glm-5.2': { context: 1000000, output: 131072 },
|
|
186
|
+
'glm-5.1': { context: 202752, output: 32768 },
|
|
187
|
+
'glm-5': { context: 202752, output: 32768 },
|
|
188
|
+
'kimi-k3': { context: 1048576, output: 131072 },
|
|
189
|
+
'kimi-k2.7-code': { context: 262144, output: 262144 },
|
|
190
|
+
'kimi-k2.6': { context: 262144, output: 65536 },
|
|
191
|
+
'kimi-k2.5': { context: 262144, output: 65536 },
|
|
192
|
+
'minimax-m3': { context: 1000000, output: 131072 },
|
|
193
|
+
'minimax-m2.7': { context: 204800, output: 131072 },
|
|
194
|
+
'minimax-m2.5': { context: 204800, output: 65536 },
|
|
195
|
+
'qwen3.8-max': { context: 1000000, output: 131072 },
|
|
196
|
+
'qwen3.7-max': { context: 1000000, output: 65536 },
|
|
197
|
+
'qwen3.7-plus': { context: 1000000, output: 65536 },
|
|
198
|
+
'qwen3.6-plus': { context: 1000000, output: 65536 },
|
|
199
|
+
'qwen3.5-plus': { context: 262144, output: 65536 },
|
|
200
|
+
'mimo-v2.5': { context: 1000000, output: 128000 },
|
|
201
|
+
'mimo-v2.5-pro': { context: 1048576, output: 128000 },
|
|
202
|
+
hy3: { context: 256000, output: 64000 },
|
|
203
|
+
'gpt-5.6-luna': { context: 1050000, output: 128000 },
|
|
204
|
+
'grok-4.5': { context: 500000, output: 500000 },
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
function opencodeGoContextFor(model) {
|
|
208
|
+
return OPENCODE_GO_LIMITS[String(model || '')]?.context;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Descrittori per l'estensione Pi generata. `compat` viene applicato SOLO ai
|
|
212
|
+
// due id per cui il repo lo dichiara gia' altrove sulla stessa wire
|
|
213
|
+
// (`pi.alibaba-token-plan`): sono gli stessi modelli su un gateway diverso,
|
|
214
|
+
// quindi e' riuso di una dichiarazione esistente. Sugli altri non c'e'
|
|
215
|
+
// precedente e non si estrapola.
|
|
216
|
+
const OPENCODE_GO_PI_COMPAT = Object.freeze(['glm-5.2', 'deepseek-v4-pro']);
|
|
217
|
+
const OPENCODE_GO_PI_MODELS = Object.freeze(OPENCODE_GO_CHAT_MODELS.map((id) => Object.freeze({
|
|
218
|
+
id,
|
|
219
|
+
name: id,
|
|
220
|
+
api: 'openai-completions',
|
|
221
|
+
reasoning: false,
|
|
222
|
+
input: ['text'],
|
|
223
|
+
contextWindow: OPENCODE_GO_LIMITS[id].context,
|
|
224
|
+
maxTokens: OPENCODE_GO_LIMITS[id].output,
|
|
225
|
+
cost: ZERO_COST,
|
|
226
|
+
...(OPENCODE_GO_PI_COMPAT.includes(id)
|
|
227
|
+
? { compat: { thinkingFormat: 'openai', requiresReasoningContentOnAssistantMessages: true } }
|
|
228
|
+
: {}),
|
|
229
|
+
})));
|
|
230
|
+
|
|
150
231
|
const CUSTOM_KEYS = ['displayName', 'protocol', 'baseUrl', 'envKey', 'providerId'];
|
|
151
|
-
const MANAGED_KEYS = new Set(['client', 'provider', 'credentialProfile', 'model', 'permissionPolicy', 'credentialSourcePolicy', ...CUSTOM_KEYS]);
|
|
232
|
+
const MANAGED_KEYS = new Set(['client', 'provider', 'credentialProfile', 'model', 'permissionPolicy', 'credentialSourcePolicy', 'envPassthrough', ...CUSTOM_KEYS]);
|
|
233
|
+
// D3: massimo numero di nomi in `envPassthrough`. L'allowlist e' opt-in e per
|
|
234
|
+
// nome, mai un passthrough in blocco: un tetto basso ferma una lista incontrollata.
|
|
235
|
+
const MAX_ENV_PASSTHROUGH = 32;
|
|
152
236
|
// Explicit credential source policy. Default 'auto' preserves the legacy
|
|
153
237
|
// resolution order (runtime -> store -> shell -> key files -> legacy) so a
|
|
154
238
|
// pre-WP1 fleet.json migrates no-op: no existing cell changes resolution.
|
|
155
239
|
const CREDENTIAL_SOURCES = Object.freeze(['environment', 'nexuscrew-store', 'auto']);
|
|
240
|
+
// I valori che `credential().source` puo' assumere, e che escono verso la UI in
|
|
241
|
+
// `describeManaged().credentialSource` / `describeCatalogCredential()`. La UI li
|
|
242
|
+
// rende con la chiave `fleet-credential-source-<valore>`, e `t()` su chiave
|
|
243
|
+
// assente restituisce LA CHIAVE: un valore nuovo senza traduzione si vede a
|
|
244
|
+
// schermo come stringa tecnica. Questa lista e' l'ancora della sonda in
|
|
245
|
+
// tests/i18n.test.js — la parita' fra le tre lingue e' gia' garantita da un
|
|
246
|
+
// altro test, qui si garantisce la COPERTURA dei valori che il backend produce.
|
|
247
|
+
// Chi aggiunge un valore in credential() aggiunge una riga qui e la stringa nei
|
|
248
|
+
// tre dizionari, oppure il gate lo ferma.
|
|
249
|
+
const CREDENTIAL_SOURCE_VALUES = Object.freeze([
|
|
250
|
+
'login', 'none', 'environment', 'nexuscrew-store', 'local', 'compatibility', 'missing', 'unreadable',
|
|
251
|
+
]);
|
|
156
252
|
const CLIENT_LABELS = Object.freeze({ claude: 'Claude Code', codex: 'Codex', 'codex-vl': 'Codex-VL', grok: 'Grok', vl: 'VL', pi: 'Pi', agy: 'Agy', kimi: 'Kimi Code CLI', shell: 'Shell' });
|
|
157
253
|
const PROVIDER_ID_RE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
158
254
|
|
|
@@ -179,7 +275,12 @@ const CATALOG = Object.freeze([
|
|
|
179
275
|
{ id: 'claude.native', client: 'claude', provider: 'native', label: 'Anthropic / Claude account', auth: 'login', endpoint: 'Anthropic account', protocol: 'anthropic_messages', rc: true, default: true, core: true },
|
|
180
276
|
{ id: 'claude.alibaba-token-plan', client: 'claude', provider: 'alibaba-token-plan', label: 'Alibaba Token Plan Personal', auth: 'ALIBABA_CODE_API_KEY', endpoint: 'https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic', protocol: 'anthropic_messages', model: 'qwen3.8-max', models: ALIBABA_TOKEN_PLAN_MODELS, strictModels: true, core: true, notice: 'alibaba-token-plan' },
|
|
181
277
|
{ id: 'claude.kimi-code', client: 'claude', provider: 'kimi-code', label: 'Kimi Code', auth: 'KIMI_API_KEY', endpoint: 'https://api.kimi.com/coding/', protocol: 'anthropic_messages', model: 'k3[1m]', models: ['k3', 'k3[1m]', 'kimi-for-coding', 'kimi-for-coding-highspeed'], strictModels: true, core: true, notice: 'claude-kimi-code' },
|
|
182
|
-
|
|
278
|
+
// GLM-5.3 dal 2026-08-15. Sul Coding Plan lo switch e' gia' automatico —
|
|
279
|
+
// chiedendo `glm-5.2` il server risponde `glm-5.3` — ma il nome scritto qui
|
|
280
|
+
// deve dire la verita' su cosa stiamo usando. Il suffisso `[1m]` resta: e' un
|
|
281
|
+
// flag di finestra del CLI e viene tolto prima della richiesta HTTP, dove
|
|
282
|
+
// `glm-5.3[1m]` letterale darebbe 400 (code 1214, misurato).
|
|
283
|
+
{ id: 'claude.zai', client: 'claude', provider: 'zai', label: 'Z.AI', auth: 'dynamic', credentialEnv: true, defaultEnvKey: 'ZAI_API_KEY', endpoint: 'https://api.z.ai/api/anthropic', protocol: 'anthropic_messages', model: 'glm-5.3[1m]', models: ['glm-5.3[1m]', 'glm-5.2[1m]'], core: true },
|
|
183
284
|
// OpenCode Go su Claude parla Anthropic Messages, e la wire accetta SOLO
|
|
184
285
|
// `x-api-key`: con `Authorization: Bearer` risponde 401 AuthError. Per questo
|
|
185
286
|
// l'endpoint e' la root senza `/v1` (il client aggiunge `/v1/messages`) e il
|
|
@@ -250,7 +351,7 @@ const CATALOG = Object.freeze([
|
|
|
250
351
|
// solo come estensione generata il cui apiKey e' `$OPENCODE_API_KEY`, che
|
|
251
352
|
// senza NexusCrew non e' valorizzato. Delegando, la cella risulterebbe
|
|
252
353
|
// configurata senza chiave e fallirebbe al primo uso.
|
|
253
|
-
{ id: 'pi.opencode-go', client: 'pi', provider: 'opencode-go', label: 'OpenCode Go', auth: 'OPENCODE_API_KEY', endpoint: OPENCODE_GO_API_BASE, protocol: 'openai-completions', model: 'deepseek-v4-flash', models: OPENCODE_GO_CHAT_MODELS, strictModels: true, piProvider: 'opencode-go', piExtension: { baseUrl: OPENCODE_GO_API_BASE }, delegatePiAuth: false, core: true },
|
|
354
|
+
{ id: 'pi.opencode-go', client: 'pi', provider: 'opencode-go', label: 'OpenCode Go', auth: 'OPENCODE_API_KEY', endpoint: OPENCODE_GO_API_BASE, protocol: 'openai-completions', model: 'deepseek-v4-flash', models: OPENCODE_GO_CHAT_MODELS, strictModels: true, piProvider: 'opencode-go', piExtension: { baseUrl: OPENCODE_GO_API_BASE, models: OPENCODE_GO_PI_MODELS }, delegatePiAuth: false, core: true },
|
|
254
355
|
{ id: 'pi.anthropic', client: 'pi', provider: 'anthropic', label: 'Anthropic', auth: 'ANTHROPIC_API_KEY', protocol: 'pi_native', piProvider: 'anthropic', core: true },
|
|
255
356
|
{ id: 'pi.openai', client: 'pi', provider: 'openai', label: 'OpenAI API', auth: 'OPENAI_API_KEY', protocol: 'pi_native', piProvider: 'openai', core: true },
|
|
256
357
|
{ id: 'pi.openai-codex', client: 'pi', provider: 'openai-codex', label: 'OpenAI Codex OAuth', auth: 'login', protocol: 'pi_native', piProvider: 'openai-codex', core: true },
|
|
@@ -259,7 +360,7 @@ const CATALOG = Object.freeze([
|
|
|
259
360
|
{ id: 'pi.github-copilot', client: 'pi', provider: 'github-copilot', label: 'GitHub Copilot', auth: 'login', protocol: 'pi_native', piProvider: 'github-copilot', core: true },
|
|
260
361
|
{ id: 'pi.deepseek', client: 'pi', provider: 'deepseek', label: 'DeepSeek', auth: 'DEEPSEEK_API_KEY', protocol: 'pi_native', piProvider: 'deepseek', core: true },
|
|
261
362
|
// Provider Pi NON core: restano fuori dal catalogo UI (publicCatalog filtra
|
|
262
|
-
// core/default/custom), in attesa di decisione
|
|
363
|
+
// core/default/custom), in attesa di una decisione di progetto. Vengono risolti solo da
|
|
263
364
|
// configurazione esistente via profileFor. Etichette senza prefisso "Pi · ".
|
|
264
365
|
{ id: 'pi.fireworks', client: 'pi', provider: 'fireworks', label: 'Fireworks AI', auth: 'FIREWORKS_API_KEY', protocol: 'pi_native', piProvider: 'fireworks' },
|
|
265
366
|
{ id: 'pi.huggingface', client: 'pi', provider: 'huggingface', label: 'Hugging Face', auth: 'HF_TOKEN', protocol: 'pi_native', piProvider: 'huggingface' },
|
|
@@ -298,8 +399,8 @@ const CATALOG = Object.freeze([
|
|
|
298
399
|
// --- Sezione legacy --------------------------------------------------------
|
|
299
400
|
// Compatibilita' sola lettura/launch per configurazioni 0.8.0: mai nel catalogo
|
|
300
401
|
// UI (publicCatalog filtra `legacy`). Risolti solo da profileFor/normalizeManagedSpec.
|
|
301
|
-
{ id: 'claude.zai-a', client: 'claude', provider: 'zai', credentialProfile: 'a', label: 'Z.AI legacy profile', auth: 'ZAI_API_KEY_A', endpoint: 'https://api.z.ai/api/anthropic', protocol: 'anthropic_messages', model: 'glm-5.
|
|
302
|
-
{ id: 'claude.zai-p', client: 'claude', provider: 'zai', credentialProfile: 'p', label: 'Z.AI legacy profile', auth: 'ZAI_API_KEY_P', endpoint: 'https://api.z.ai/api/anthropic', protocol: 'anthropic_messages', model: 'glm-5.
|
|
402
|
+
{ id: 'claude.zai-a', client: 'claude', provider: 'zai', credentialProfile: 'a', label: 'Z.AI legacy profile', auth: 'ZAI_API_KEY_A', endpoint: 'https://api.z.ai/api/anthropic', protocol: 'anthropic_messages', model: 'glm-5.3[1m]', models: ['glm-5.3[1m]', 'glm-5.2[1m]'], legacySecrets: true, legacyProvider: 'zai-a', legacy: true },
|
|
403
|
+
{ id: 'claude.zai-p', client: 'claude', provider: 'zai', credentialProfile: 'p', label: 'Z.AI legacy profile', auth: 'ZAI_API_KEY_P', endpoint: 'https://api.z.ai/api/anthropic', protocol: 'anthropic_messages', model: 'glm-5.3[1m]', models: ['glm-5.3[1m]', 'glm-5.2[1m]'], legacySecrets: true, legacyProvider: 'zai-p', legacy: true },
|
|
303
404
|
]);
|
|
304
405
|
|
|
305
406
|
function profileFor(client, provider, credentialProfile) {
|
|
@@ -370,6 +471,27 @@ function normalizeManagedSpec(value, { extraModels = null } = {}) {
|
|
|
370
471
|
if (!model || !(profile.protocols || [profile.protocol]).includes(protocol)) return null;
|
|
371
472
|
Object.assign(out, { displayName, baseUrl, envKey, protocol, providerId });
|
|
372
473
|
}
|
|
474
|
+
// D3: envPassthrough e' un'allowlist di NOMI di variabili d'ambiente che il
|
|
475
|
+
// child deve ricevere, risolti a runtime dalle credentialSources (dopo i rami
|
|
476
|
+
// provider in resolveManagedEngine). E' opt-in e per nome: MAI un passthrough
|
|
477
|
+
// in blocco dell'ambiente, solo i nomi elencati — ciascuno un nome env valido.
|
|
478
|
+
// E il mezzo con cui una cella vl (auth 'none', ramo senza env provider) riceve
|
|
479
|
+
// le variabili che il suo runtime legge: il nome non e' fisso nel codice vl
|
|
480
|
+
// (vivling/src/main.rs), quindi lo dichiara l'operatore che conosce la sua config.
|
|
481
|
+
if (value.envPassthrough !== undefined) {
|
|
482
|
+
if (!Array.isArray(value.envPassthrough) || !value.envPassthrough.length
|
|
483
|
+
|| value.envPassthrough.length > MAX_ENV_PASSTHROUGH) return null;
|
|
484
|
+
const seen = new Set();
|
|
485
|
+
const names = [];
|
|
486
|
+
for (const raw of value.envPassthrough) {
|
|
487
|
+
if (typeof raw !== 'string') return null;
|
|
488
|
+
const name = raw.trim();
|
|
489
|
+
if (!ENV_KEY_RE.test(name) || seen.has(name)) return null;
|
|
490
|
+
seen.add(name);
|
|
491
|
+
names.push(name);
|
|
492
|
+
}
|
|
493
|
+
out.envPassthrough = names;
|
|
494
|
+
}
|
|
373
495
|
return out;
|
|
374
496
|
}
|
|
375
497
|
|
|
@@ -477,7 +599,17 @@ function safeAllowedRoots(roots = []) {
|
|
|
477
599
|
return out;
|
|
478
600
|
}
|
|
479
601
|
|
|
480
|
-
|
|
602
|
+
// `out.blocked` (accumulatore opzionale) raccoglie i file CHE ESISTONO ma non si
|
|
603
|
+
// sono potuti leggere/verificare (EACCES/ELOOP/ENOTDIR/EIO...). I rifiuti
|
|
604
|
+
// deliberati (symlink fuori roots, mode/uid/size non validi) restano `return {}`
|
|
605
|
+
// espliciti — sono legittimi "questo file non e' una credenziale valida" — e NON
|
|
606
|
+
// finiscono nei blocked. ENOENT nel catch e' "il file non c'e'" (legittimo, niente
|
|
607
|
+
// valore); solo gli altri code sono "non ho potuto guardare", e vanno distinti dal
|
|
608
|
+
// "missing" che il caller altrimenti riporterebbe per una credenziale presente ma
|
|
609
|
+
// illeggibile. Il verdetto (niente valori estratti -> {}) e' invariato; il
|
|
610
|
+
// discriminante e' CHI ha fallito, non che ci sia stata un'eccezione.
|
|
611
|
+
function parseEnvFile(file, opts = {}, out) {
|
|
612
|
+
const blocked = Array.isArray(out && out.blocked) ? out.blocked : null;
|
|
481
613
|
try {
|
|
482
614
|
const lst = fs.lstatSync(file);
|
|
483
615
|
let target = file;
|
|
@@ -491,16 +623,23 @@ function parseEnvFile(file, opts = {}) {
|
|
|
491
623
|
if (!st.isFile() || st.isSymbolicLink() || (st.mode & 0o077) || st.size > 256 * 1024) return {};
|
|
492
624
|
if (typeof process.getuid === 'function' && st.uid !== process.getuid()) return {};
|
|
493
625
|
return parseAssignments(fs.readFileSync(target, 'utf8'));
|
|
494
|
-
} catch (
|
|
626
|
+
} catch (e) {
|
|
627
|
+
if (blocked && e.code !== 'ENOENT') blocked.push({ path: file, code: e.code || e.constructor.name });
|
|
628
|
+
return {};
|
|
629
|
+
}
|
|
495
630
|
}
|
|
496
631
|
|
|
497
|
-
function parseProviderShellFile(file) {
|
|
632
|
+
function parseProviderShellFile(file, out) {
|
|
633
|
+
const blocked = Array.isArray(out && out.blocked) ? out.blocked : null;
|
|
498
634
|
try {
|
|
499
635
|
const st = fs.lstatSync(file);
|
|
500
636
|
if (!st.isFile() || st.isSymbolicLink() || (st.mode & 0o022) || st.size > 256 * 1024) return {};
|
|
501
637
|
if (typeof process.getuid === 'function' && st.uid !== process.getuid()) return {};
|
|
502
638
|
return parseAssignments(fs.readFileSync(file, 'utf8'));
|
|
503
|
-
} catch (
|
|
639
|
+
} catch (e) {
|
|
640
|
+
if (blocked && e.code !== 'ENOENT') blocked.push({ path: file, code: e.code || e.constructor.name });
|
|
641
|
+
return {};
|
|
642
|
+
}
|
|
504
643
|
}
|
|
505
644
|
|
|
506
645
|
function binaryCandidates(client, home) {
|
|
@@ -517,14 +656,28 @@ function binaryCandidates(client, home) {
|
|
|
517
656
|
].filter(Boolean))];
|
|
518
657
|
}
|
|
519
658
|
|
|
520
|
-
|
|
659
|
+
// `out.blocked` (accumulatore opzionale) raccoglie i candidati CHE ESISTONO come
|
|
660
|
+
// nome nel PATH ma che non si sono potuti VERIFICARE (EACCES/ELOOP/ENOTDIR...),
|
|
661
|
+
// non i candidati assenti (ENOENT = legittimo "prossimo"). Il verdetto del caller
|
|
662
|
+
// non cambia: findBinary torna comunque null se nessun candidato e' confermato
|
|
663
|
+
// (non possiamo dichiararlo "trovato"); ma chi costruisce il messaggio
|
|
664
|
+
// (describeManaged) puo' ora distinguere "client non trovato" da "non ho potuto
|
|
665
|
+
// verificare un candidato" — il discriminante e' CHI ha fallito, non che ci sia
|
|
666
|
+
// stata un'eccezione. Stesso principio gia' applicato in checkTermuxExec (3134d2f).
|
|
667
|
+
function findBinary(client, home, out) {
|
|
668
|
+
const blocked = Array.isArray(out && out.blocked) ? out.blocked : null;
|
|
521
669
|
for (const candidate of binaryCandidates(client, home)) {
|
|
522
670
|
try {
|
|
523
671
|
const real = fs.realpathSync(candidate); const st = fs.lstatSync(real);
|
|
524
672
|
if (!st.isFile() || !(st.mode & 0o100) || (st.mode & 0o002)) continue;
|
|
525
673
|
if (typeof process.getuid === 'function' && st.uid !== process.getuid() && st.uid !== 0) continue;
|
|
526
674
|
return real;
|
|
527
|
-
} catch (
|
|
675
|
+
} catch (e) {
|
|
676
|
+
// ENOENT = il candidato non esiste (legittimo "prossimo"); qualsiasi altro
|
|
677
|
+
// code (EACCES/ELOOP/ENOTDIR/EIO...) = "esiste ma non ho potuto guardarlo",
|
|
678
|
+
// e va distinto dal "non trovato" finale, non collassato in "prossimo".
|
|
679
|
+
if (blocked && e.code !== 'ENOENT') blocked.push({ path: candidate, code: e.code || e.constructor.name });
|
|
680
|
+
}
|
|
528
681
|
}
|
|
529
682
|
return null;
|
|
530
683
|
}
|
|
@@ -532,7 +685,7 @@ function findBinary(client, home) {
|
|
|
532
685
|
// Resolve a device-local interactive shell without persisting a path in
|
|
533
686
|
// fleet.json. Candidates are ordered and fail closed. Symlinks are resolved
|
|
534
687
|
// first, then the existing command trust policy is applied to the real file.
|
|
535
|
-
function resolveInteractiveShell(cfg = {}) {
|
|
688
|
+
function resolveInteractiveShell(cfg = {}, out) {
|
|
536
689
|
const env = cfg.env || process.env;
|
|
537
690
|
const platform = cfg.platform || process.platform;
|
|
538
691
|
const termux = termuxRuntimePaths(env, { platform, home: cfg.home });
|
|
@@ -545,11 +698,19 @@ function resolveInteractiveShell(cfg = {}) {
|
|
|
545
698
|
candidates.push('/bin/bash', '/bin/sh');
|
|
546
699
|
const validate = cfg.validateCommandTrust
|
|
547
700
|
|| ((command) => require('./definitions.js').validateCommandTrust(command));
|
|
701
|
+
const blocked = Array.isArray(out && out.blocked) ? out.blocked : null;
|
|
548
702
|
for (const candidate of [...new Set(candidates)]) {
|
|
549
703
|
try {
|
|
550
704
|
const real = fs.realpathSync(candidate);
|
|
551
705
|
if (validate(real).ok) return real;
|
|
552
|
-
} catch (
|
|
706
|
+
} catch (e) {
|
|
707
|
+
// ENOENT = il candidato non esiste (legittimo "prossimo"); altro code
|
|
708
|
+
// (EACCES/ELOOP/ENOTDIR...) = "non ho potuto verificare", da distinguere
|
|
709
|
+
// dal "nessuna shell" finale. validate() che torna ok:false NON e' un
|
|
710
|
+
// throw: e' un rifiuto legittimo (candidato presente ma non fidato), resta
|
|
711
|
+
// "prossimo" senza finire nei blocked — il discriminante e' CHI ha fallito.
|
|
712
|
+
if (blocked && e.code !== 'ENOENT') blocked.push({ path: candidate, code: e.code || e.constructor.name });
|
|
713
|
+
}
|
|
553
714
|
}
|
|
554
715
|
return null;
|
|
555
716
|
}
|
|
@@ -614,34 +775,40 @@ function providerKeyPaths(cfg, home) {
|
|
|
614
775
|
return [...new Set(paths.filter((file) => typeof file === 'string' && file))];
|
|
615
776
|
}
|
|
616
777
|
|
|
617
|
-
function parseProviderKeyFiles(cfg, home) {
|
|
778
|
+
function parseProviderKeyFiles(cfg, home, out) {
|
|
618
779
|
const values = {};
|
|
619
780
|
// Match providers.zsh ordering: a later secure file may intentionally
|
|
620
781
|
// override the canonical ai.env value. Files remain data-only and must be
|
|
621
782
|
// private regular files owned by the NexusCrew user.
|
|
622
783
|
const files = providerKeyPaths(cfg, home);
|
|
623
784
|
const roots = [...new Set(files.map((file) => path.dirname(path.resolve(file))))];
|
|
624
|
-
for (const file of files) Object.assign(values, parseEnvFile(file, { allowSymlinkRoots: roots }));
|
|
785
|
+
for (const file of files) Object.assign(values, parseEnvFile(file, { allowSymlinkRoots: roots }, out));
|
|
625
786
|
return values;
|
|
626
787
|
}
|
|
627
788
|
|
|
628
|
-
|
|
789
|
+
// `trackLegacy`: il file legacy viene letto sempre, ma e' una FONTE solo per i
|
|
790
|
+
// profili con legacySecrets. Tracciarne l'illeggibilita' anche per gli altri
|
|
791
|
+
// produce un messaggio che manda l'operatore a sistemare un permesso
|
|
792
|
+
// irrilevante — «non verificabile» su un file che per quella chiave non conta
|
|
793
|
+
// nulla, mentre la chiave e' davvero assente. Il tracciamento segue la fonte,
|
|
794
|
+
// non la lettura.
|
|
795
|
+
function credentialSources(cfg, home, out, { trackLegacy = true } = {}) {
|
|
629
796
|
let local = {};
|
|
630
797
|
try { local = readCredentialStore(cfg, home); } catch (_) { /* unsafe/corrupt store is ignored, never trusted */ }
|
|
631
798
|
return {
|
|
632
799
|
runtime: cfg.env || process.env,
|
|
633
800
|
local,
|
|
634
|
-
shell: parseProviderShellFile(shellProvidersPath(cfg, home)),
|
|
635
|
-
keys: parseProviderKeyFiles(cfg, home),
|
|
636
|
-
legacy: parseEnvFile(secretsPath(cfg, home)),
|
|
801
|
+
shell: parseProviderShellFile(shellProvidersPath(cfg, home), out),
|
|
802
|
+
keys: parseProviderKeyFiles(cfg, home, out),
|
|
803
|
+
legacy: parseEnvFile(secretsPath(cfg, home), {}, trackLegacy ? out : undefined),
|
|
637
804
|
};
|
|
638
805
|
}
|
|
639
806
|
|
|
640
|
-
function credential(profile, spec, cfg, home) {
|
|
807
|
+
function credential(profile, spec, cfg, home, out) {
|
|
641
808
|
if (profile.auth === 'login' || profile.auth === 'none') return { envKey: profile.auth, value: '', source: profile.auth };
|
|
642
809
|
const envKey = profile.auth === 'dynamic' ? spec.envKey : profile.auth;
|
|
643
810
|
const policy = spec && CREDENTIAL_SOURCES.includes(spec.credentialSourcePolicy) ? spec.credentialSourcePolicy : 'auto';
|
|
644
|
-
const sources = credentialSources(cfg, home);
|
|
811
|
+
const sources = credentialSources(cfg, home, out, { trackLegacy: !!profile.legacySecrets });
|
|
645
812
|
// The fixed shell file is already the user's environment source. Values are
|
|
646
813
|
// consumed only in memory and passed to the selected child; never persisted
|
|
647
814
|
// in fleet.json, service files, API responses or logs.
|
|
@@ -661,7 +828,15 @@ function credential(profile, spec, cfg, home) {
|
|
|
661
828
|
if (profile.legacySecrets && sources.legacy[envKey]) {
|
|
662
829
|
return { envKey, value: sources.legacy[envKey], source: 'compatibility' };
|
|
663
830
|
}
|
|
664
|
-
|
|
831
|
+
// auto: nessuna fonte ha la chiave. Se un file credenziale esiste ma non si
|
|
832
|
+
// e' potuto leggere (EACCES/ELOOP...), non possiamo dichiarare la chiave
|
|
833
|
+
// "missing" (che implica "mettila su questo device"): e' "unreadable", non
|
|
834
|
+
// verificata. Il verdetto (niente valore -> authConfigured false) e' invariato;
|
|
835
|
+
// il messaggio di describeManaged lo distingue. environment/nexuscrew-store
|
|
836
|
+
// sopra restano 'missing': le loro fonti (runtime env / local store) non
|
|
837
|
+
// passano per i file parseEnvFile/parseProviderShellFile che tracciamo qui.
|
|
838
|
+
const unreadable = out && Array.isArray(out.blocked) && out.blocked.length;
|
|
839
|
+
return { envKey, value: '', source: unreadable ? 'unreadable' : 'missing' };
|
|
665
840
|
}
|
|
666
841
|
|
|
667
842
|
// The profile's "owned" env set. When the credential source is the local store,
|
|
@@ -781,22 +956,54 @@ async function discoverPiModels(opts = {}) {
|
|
|
781
956
|
function describeManaged(spec, cfg = {}) {
|
|
782
957
|
const extraModels = cfg.extraModels || null;
|
|
783
958
|
const normalized = normalizeManagedSpec(spec, { extraModels });
|
|
784
|
-
if (!normalized)
|
|
959
|
+
if (!normalized) {
|
|
960
|
+
// D2: il rifiuto resta (i descrittori NON appartengono al profilo di una
|
|
961
|
+
// cella: vengono dalla definizione dell'ENGINE, in `d.models` — due
|
|
962
|
+
// soggetti diversi, mescolarli renderebbe ambiguo chi dichiara cosa). Ma
|
|
963
|
+
// se la causa e' proprio questa, il messaggio generico "invalid managed
|
|
964
|
+
// profile" manda a cercare ovunque tranne che nel posto giusto: dice
|
|
965
|
+
// dove i descrittori vanno davvero.
|
|
966
|
+
if (spec && typeof spec === 'object' && !Array.isArray(spec)
|
|
967
|
+
&& Object.prototype.hasOwnProperty.call(spec, 'models')) {
|
|
968
|
+
return {
|
|
969
|
+
configured: false,
|
|
970
|
+
reason: '"models" non e\' un campo del profilo managed della cella — i descrittori dei modelli si dichiarano nella definizione dell\'ENGINE, nell\'array "models" del documento (schemaVersion/engines/cells/models), non qui',
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
return { configured: false, reason: 'invalid managed profile' };
|
|
974
|
+
}
|
|
785
975
|
const home = cfg.home || require('node:os').homedir();
|
|
786
976
|
const profile = profileFor(normalized.client, normalized.provider, normalized.credentialProfile || '');
|
|
977
|
+
// Tracciamento di candidati (binary) e file credenziali che ESISTONO ma non
|
|
978
|
+
// si sono potuti VERIFICARE (EACCES/ELOOP/ENOTDIR...). Il verdetto
|
|
979
|
+
// (configured/authConfigured) non cambia: il discriminante e' CHI ha fallito,
|
|
980
|
+
// non che ci sia stata un'eccezione. ENOENT = legittimo "non c'e'"; altro code
|
|
981
|
+
// = "non ho potuto guardare", e il messaggio deve dirlo invece di collassarlo
|
|
982
|
+
// in "not found" / "missing" (la stessa forma gia' chiusa in checkTermuxExec).
|
|
983
|
+
const binaryBlocked = [];
|
|
984
|
+
const credBlocked = [];
|
|
787
985
|
const binary = normalized.client === 'shell'
|
|
788
|
-
? resolveInteractiveShell({ ...cfg, home })
|
|
789
|
-
: findBinary(normalized.client, home);
|
|
790
|
-
const cred = credential(profile, normalized, cfg, home);
|
|
986
|
+
? resolveInteractiveShell({ ...cfg, home }, { blocked: binaryBlocked })
|
|
987
|
+
: findBinary(normalized.client, home, { blocked: binaryBlocked });
|
|
988
|
+
const cred = credential(profile, normalized, cfg, home, { blocked: credBlocked });
|
|
791
989
|
// Pi can resolve credentials from its own documented /login auth store. Do
|
|
792
990
|
// not inspect or copy that store; delegate native-provider auth to Pi.
|
|
793
991
|
const delegatedPiAuth = profile.client === 'pi' && profile.provider !== 'custom'
|
|
794
992
|
&& profile.delegatePiAuth !== false;
|
|
795
993
|
const authConfigured = delegatedPiAuth || profile.auth === 'login' || profile.auth === 'none' || !!cred.value;
|
|
796
994
|
let configured = !!binary && authConfigured;
|
|
797
|
-
let reason
|
|
798
|
-
|
|
799
|
-
|
|
995
|
+
let reason;
|
|
996
|
+
if (!binary) {
|
|
997
|
+
reason = binaryBlocked.length
|
|
998
|
+
? `client ${profile.client} not confirmed: ${binaryBlocked.length === 1 ? 'a candidate could not be verified' : 'some candidates could not be verified'} (${binaryBlocked.map((b) => `${b.path} (${b.code})`).join('; ')}) — not "absent"`
|
|
999
|
+
: `client ${profile.client} not found`;
|
|
1000
|
+
} else if (!authConfigured) {
|
|
1001
|
+
reason = cred.source === 'unreadable'
|
|
1002
|
+
? `credential ${cred.envKey} not verifiable (file present but unreadable: ${credBlocked.map((b) => `${b.path} (${b.code})`).join('; ')}) — not "missing"`
|
|
1003
|
+
: `credential ${cred.envKey} missing — set it on this device`;
|
|
1004
|
+
} else {
|
|
1005
|
+
reason = 'ready';
|
|
1006
|
+
}
|
|
800
1007
|
// Agy e grok sono client primari supportati solo su Linux/macOS non-Termux.
|
|
801
1008
|
// Rilevazione Termux via termuxRuntimePaths (non solo process.platform): un
|
|
802
1009
|
// Node che riporta 'linux' sotto proot/Termux viene comunque intercettato.
|
|
@@ -814,7 +1021,11 @@ function describeManaged(spec, cfg = {}) {
|
|
|
814
1021
|
permissionPolicy: normalized.permissionPolicy, protocol: normalized.protocol || profile.protocol,
|
|
815
1022
|
endpoint: normalized.baseUrl || profile.endpoint || '', auth: cred.envKey, authConfigured,
|
|
816
1023
|
credentialSourcePolicy: normalized.credentialSourcePolicy || 'auto',
|
|
817
|
-
|
|
1024
|
+
// cred.source e' gia' 'missing' quando non c'e' valore: non serve il
|
|
1025
|
+
// ternario che lo forzava. Cosi' una credenziale presente ma illeggibile
|
|
1026
|
+
// (source 'unreadable', authConfigured false) non viene collassata in
|
|
1027
|
+
// 'missing' — il discriminante e' CHI ha fallito, non il esito grezzo.
|
|
1028
|
+
credentialSource: cred.source,
|
|
818
1029
|
configured, models: [...(profile.models || [])], defaultModel: profile.model || '',
|
|
819
1030
|
binary: binary || '', displayName: normalized.displayName || profile.label,
|
|
820
1031
|
reason,
|
|
@@ -829,11 +1040,14 @@ function describeCatalogCredential(client, provider, credentialProfile = '', cfg
|
|
|
829
1040
|
if (!profile || profile.auth === 'dynamic' || profile.auth === 'login' || profile.auth === 'none'
|
|
830
1041
|
|| !ENV_KEY_RE.test(profile.auth || '')) return null;
|
|
831
1042
|
const home = cfg.home || require('node:os').homedir();
|
|
832
|
-
const cred = credential(profile, {}, cfg, home);
|
|
1043
|
+
const cred = credential(profile, {}, cfg, home, { blocked: [] });
|
|
833
1044
|
return {
|
|
834
1045
|
envKey: cred.envKey,
|
|
835
1046
|
authConfigured: !!cred.value,
|
|
836
|
-
|
|
1047
|
+
// cred.source e' gia' 'missing' quando non c'e' valore: non serve il
|
|
1048
|
+
// ternario che lo forzava. Cosi' una credenziale presente ma illeggibile
|
|
1049
|
+
// (source 'unreadable') non viene collassata in 'missing'.
|
|
1050
|
+
credentialSource: cred.source,
|
|
837
1051
|
};
|
|
838
1052
|
}
|
|
839
1053
|
|
|
@@ -1033,7 +1247,41 @@ function ensurePrivateClaudeConfig(home, profileId, label, penguinMode) {
|
|
|
1033
1247
|
return configDir;
|
|
1034
1248
|
}
|
|
1035
1249
|
|
|
1036
|
-
|
|
1250
|
+
// D2 audit: il file .ts generato NON e' il consumatore — Pi lo E'. Un test che
|
|
1251
|
+
// legge solo il file resta verde su un'estensione che Pi rifiuta a runtime.
|
|
1252
|
+
// CONTRATTO REALE che Pi (@earendil-works/pi-coding-agent 0.80.10) impone a
|
|
1253
|
+
// ogni modello di `pi.registerProvider(id, {models: [...]})` — fonte:
|
|
1254
|
+
// core/extensions/types.d.ts, interface ProviderModelConfig (JSDoc del pacchetto
|
|
1255
|
+
// installato, non dedotto). Campi OBBLIGATORI: id, name, reasoning, input
|
|
1256
|
+
// (array "text"|"image"), cost ({input,output,cacheRead,cacheWrite}),
|
|
1257
|
+
// contextWindow, maxTokens. `input` mancante fa THROW dentro Pi la prima volta
|
|
1258
|
+
// che un tool consulta le capability del modello (es. core/tools/read.js:
|
|
1259
|
+
// `model.input.includes("image")` — TypeError su undefined, misurato e
|
|
1260
|
+
// riprodotto con il modulo Pi reale). I descrittori grezzi dichiarati
|
|
1261
|
+
// dall'operatore in `d.models` (via parseModel: id, engine, label?,
|
|
1262
|
+
// contextWindow, maxTokens, reasoning) NON hanno name/input/cost — vanno
|
|
1263
|
+
// arricchiti PRIMA di finire nell'estensione, mai passati cosi' come sono.
|
|
1264
|
+
function toPiModelConfig(m) {
|
|
1265
|
+
return {
|
|
1266
|
+
id: m.id,
|
|
1267
|
+
name: m.label || m.id,
|
|
1268
|
+
reasoning: m.reasoning === true,
|
|
1269
|
+
input: ['text'],
|
|
1270
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
1271
|
+
contextWindow: m.contextWindow || 128000,
|
|
1272
|
+
maxTokens: m.maxTokens || 16384,
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// D2: `declaredModels` (opzionale) sono i descrittori dichiarati per l'ENGINE
|
|
1277
|
+
// (via declaredModelsFor), per il ramo Pi CUSTOM — stesso motivo/stessa fonte
|
|
1278
|
+
// di customCatalogFor, mai letti da `spec.models`. Il ramo Pi NON-custom
|
|
1279
|
+
// (profile.piExtension) continua a passare `models` dentro l'oggetto spec-like
|
|
1280
|
+
// come sempre: e' un catalogo STATICO cablato nel codice (es. alibaba-token-
|
|
1281
|
+
// plan), GIA' nella forma completa che Pi si aspetta — un caso diverso, non
|
|
1282
|
+
// toccato dal difetto D2. declaredModels, quando presente, ha priorita', ed e'
|
|
1283
|
+
// sempre passato per toPiModelConfig (mai i descrittori grezzi cosi' come sono).
|
|
1284
|
+
function writePiProviderExtension(spec, home, declaredModels) {
|
|
1037
1285
|
const dir = path.join(home, '.nexuscrew', 'pi-providers');
|
|
1038
1286
|
try {
|
|
1039
1287
|
const st = fs.lstatSync(dir);
|
|
@@ -1055,7 +1303,8 @@ function writePiProviderExtension(spec, home) {
|
|
|
1055
1303
|
apiKey: spec.apiKey || `$${spec.envKey}`,
|
|
1056
1304
|
authHeader: true,
|
|
1057
1305
|
api: spec.protocol,
|
|
1058
|
-
models: Array.isArray(
|
|
1306
|
+
models: (Array.isArray(declaredModels) && declaredModels.length ? declaredModels.map(toPiModelConfig)
|
|
1307
|
+
: (Array.isArray(spec.models) && spec.models.length ? spec.models : null)) || [{
|
|
1059
1308
|
id: spec.model, name: spec.model, reasoning: false, input: ['text'],
|
|
1060
1309
|
contextWindow: 128000, maxTokens: 16384,
|
|
1061
1310
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
@@ -1074,6 +1323,87 @@ function writePiProviderExtension(spec, home) {
|
|
|
1074
1323
|
return target;
|
|
1075
1324
|
}
|
|
1076
1325
|
|
|
1326
|
+
// D2 (fix definitivo — l'audit del pacchetto aveva bocciato la prima versione:
|
|
1327
|
+
// il test costruiva `spec.models` a mano, e in produzione quel campo non
|
|
1328
|
+
// esiste mai). `declaredModels` NON viene da `spec`: i descrittori sono
|
|
1329
|
+
// proprieta' della definizione dell'ENGINE (`d.models` del documento), non del
|
|
1330
|
+
// profilo managed della cella — due soggetti diversi. Il chiamante
|
|
1331
|
+
// (resolveManagedEngine) li ricava con declaredModelsFor(extraModels,
|
|
1332
|
+
// profile.id) e li passa qui espliciti, cosi' questa funzione non finge mai
|
|
1333
|
+
// che `spec` porti qualcosa che semanticamente non gli appartiene.
|
|
1334
|
+
// Deriva model_catalog_json e model_context_window dal descrittore, così Codex-VL
|
|
1335
|
+
// non ricade sul fallback 272K (-73% finestra) con parallel tool call assenti.
|
|
1336
|
+
// Se non ci sono descrittori dichiarati -> null (comportamento invariato,
|
|
1337
|
+
// NESSUNA regressione per chi non li usa). NON consacra
|
|
1338
|
+
// ~/.codex/custom_provider_model_catalog.json: era una patch locale di una
|
|
1339
|
+
// singola installazione, non un contratto; il catalog è generato dai descrittori dichiarati
|
|
1340
|
+
// in fleet.json. I valori enum/default rispecchiano i cataloghi spediti
|
|
1341
|
+
// (validati dal test fleet-catalog-schema).
|
|
1342
|
+
function customCatalogFor(spec, model, declaredModels, home) {
|
|
1343
|
+
const models = Array.isArray(declaredModels) ? declaredModels : [];
|
|
1344
|
+
if (!models.length) return null;
|
|
1345
|
+
const entry = models.find((m) => m && m.id === model) || models[0];
|
|
1346
|
+
const cat = {
|
|
1347
|
+
models: models.map((m) => {
|
|
1348
|
+
const reasoning = m.reasoning === true;
|
|
1349
|
+
return {
|
|
1350
|
+
slug: m.id,
|
|
1351
|
+
display_name: m.label || m.id,
|
|
1352
|
+
description: m.label || m.id,
|
|
1353
|
+
default_reasoning_level: reasoning ? 'high' : 'medium',
|
|
1354
|
+
supported_reasoning_levels: reasoning
|
|
1355
|
+
? [{ effort: 'low', description: 'Fast responses with lighter reasoning' },
|
|
1356
|
+
{ effort: 'high', description: 'Greater reasoning depth for complex problems' },
|
|
1357
|
+
{ effort: 'max', description: 'Maximum reasoning depth' }]
|
|
1358
|
+
: [{ effort: 'low', description: 'Fast responses with lighter reasoning' },
|
|
1359
|
+
{ effort: 'medium', description: 'Balanced reasoning depth' },
|
|
1360
|
+
{ effort: 'high', description: 'Greater reasoning depth for complex problems' }],
|
|
1361
|
+
shell_type: 'default',
|
|
1362
|
+
visibility: 'list',
|
|
1363
|
+
supported_in_api: true,
|
|
1364
|
+
priority: 50,
|
|
1365
|
+
availability_nux: null,
|
|
1366
|
+
upgrade: null,
|
|
1367
|
+
base_instructions: '',
|
|
1368
|
+
supports_reasoning_summaries: true,
|
|
1369
|
+
default_reasoning_summary: 'none',
|
|
1370
|
+
support_verbosity: false,
|
|
1371
|
+
default_verbosity: null,
|
|
1372
|
+
apply_patch_tool_type: null,
|
|
1373
|
+
web_search_tool_type: 'text',
|
|
1374
|
+
truncation_policy: { mode: 'tokens', limit: m.maxTokens || m.contextWindow || 128000 },
|
|
1375
|
+
supports_parallel_tool_calls: false,
|
|
1376
|
+
supports_image_detail_original: false,
|
|
1377
|
+
context_window: m.contextWindow || 128000,
|
|
1378
|
+
effective_context_window_percent: 95,
|
|
1379
|
+
experimental_supported_tools: [],
|
|
1380
|
+
input_modalities: ['text'],
|
|
1381
|
+
supports_search_tool: false,
|
|
1382
|
+
};
|
|
1383
|
+
}),
|
|
1384
|
+
};
|
|
1385
|
+
const dir = path.join(home, '.nexuscrew', 'custom-catalogs');
|
|
1386
|
+
try {
|
|
1387
|
+
const st = fs.lstatSync(dir);
|
|
1388
|
+
if (st.isSymbolicLink() || !st.isDirectory()) throw new Error('unsafe custom catalog directory');
|
|
1389
|
+
} catch (e) {
|
|
1390
|
+
if (e.code === 'ENOENT') fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
1391
|
+
else throw e;
|
|
1392
|
+
}
|
|
1393
|
+
fs.chmodSync(dir, 0o700);
|
|
1394
|
+
const target = path.join(dir, `${spec.providerId}.json`);
|
|
1395
|
+
const tmp = path.join(dir, `.${spec.providerId}.${crypto.randomBytes(6).toString('hex')}.tmp`);
|
|
1396
|
+
try {
|
|
1397
|
+
fs.writeFileSync(tmp, JSON.stringify(cat), { mode: 0o600 });
|
|
1398
|
+
fs.chmodSync(tmp, 0o600);
|
|
1399
|
+
fs.renameSync(tmp, target);
|
|
1400
|
+
} catch (e) {
|
|
1401
|
+
try { fs.unlinkSync(tmp); } catch (_) {}
|
|
1402
|
+
throw e;
|
|
1403
|
+
}
|
|
1404
|
+
return { catalogPath: target, contextWindow: entry.contextWindow || 128000 };
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1077
1407
|
function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
1078
1408
|
// `extraModels` DEVE arrivare fin qui. Threadarlo al chiamante e alla vista
|
|
1079
1409
|
// non basta: la normalizzazione che decide se la cella PARTE e' questa, e
|
|
@@ -1086,6 +1416,12 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
1086
1416
|
if (!spec || !info.configured) return { ok: false, reason: info.reason, info };
|
|
1087
1417
|
const home = cfg.home || require('node:os').homedir();
|
|
1088
1418
|
const profile = profileFor(spec.client, spec.provider, spec.credentialProfile || '');
|
|
1419
|
+
// D2: i descrittori dichiarati per QUESTO profilo (client.provider — la
|
|
1420
|
+
// stessa chiave che declaredFor usa dentro normalizeManagedSpec per
|
|
1421
|
+
// validare gli id). Vengono dalla definizione dell'ENGINE (extraModels),
|
|
1422
|
+
// MAI da spec: e' il ponte che customCatalogFor/writePiProviderExtension
|
|
1423
|
+
// (rami custom) usano per emettere catalogo e finestra di contesto.
|
|
1424
|
+
const declaredModels = declaredModelsFor(cfg.extraModels, profile.id);
|
|
1089
1425
|
const cred = credential(profile, spec, cfg, home);
|
|
1090
1426
|
// L'override PER-CELLA va canonicalizzato come lo spec: `normalizeManagedSpec`
|
|
1091
1427
|
// applica l'alias a `spec.model`, ma `cell.model` lo scavalca DOPO e senza
|
|
@@ -1151,7 +1487,11 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
1151
1487
|
CLAUDE_CODE_MAX_CONTEXT_TOKENS: contextWindow,
|
|
1152
1488
|
API_TIMEOUT_MS: '3000000',
|
|
1153
1489
|
});
|
|
1154
|
-
|
|
1490
|
+
// Effort massimo di default dove il modello lo sfrutta davvero. Per
|
|
1491
|
+
// GLM-5.3 la misura di Z.AI dice che alzando l'effort l'accuratezza sale
|
|
1492
|
+
// E i token per task scendono rispetto a 5.2: con una finestra a tempo,
|
|
1493
|
+
// spendere di piu' per chiamata rende di piu' per finestra.
|
|
1494
|
+
if (model === 'k3' || model === 'k3[1m]' || model.startsWith('glm-5.3')) {
|
|
1155
1495
|
env.CLAUDE_CODE_EFFORT_LEVEL = 'max';
|
|
1156
1496
|
env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT = '1';
|
|
1157
1497
|
}
|
|
@@ -1161,9 +1501,6 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
1161
1501
|
// ANTHROPIC_API_KEY e' cio' che Claude Code manda come `x-api-key`, quindi
|
|
1162
1502
|
// qui NON si usa la forma a token degli altri gateway.
|
|
1163
1503
|
//
|
|
1164
|
-
// Nessun CLAUDE_CODE_MAX_CONTEXT_TOKENS: il context window reale di questi
|
|
1165
|
-
// modelli su OpenCode Go non e' misurato, e un numero inventato qui
|
|
1166
|
-
// diventerebbe la configurazione con cui le celle compattano.
|
|
1167
1504
|
Object.assign(env, {
|
|
1168
1505
|
ANTHROPIC_BASE_URL: profile.endpoint,
|
|
1169
1506
|
ANTHROPIC_API_KEY: cred.value,
|
|
@@ -1175,6 +1512,14 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
1175
1512
|
CLAUDE_CODE_SUBAGENT_MODEL: model,
|
|
1176
1513
|
API_TIMEOUT_MS: '3000000',
|
|
1177
1514
|
});
|
|
1515
|
+
// Il contesto si dichiara solo se il modello e' nella tabella: un id
|
|
1516
|
+
// fuori tabella non deve ereditare il numero di un altro modello, e
|
|
1517
|
+
// l'assenza fa ricadere il client sul proprio default.
|
|
1518
|
+
const opencodeContext = opencodeGoContextFor(model);
|
|
1519
|
+
if (opencodeContext) {
|
|
1520
|
+
env.CLAUDE_CODE_MAX_CONTEXT_TOKENS = String(opencodeContext);
|
|
1521
|
+
env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = String(opencodeContext);
|
|
1522
|
+
}
|
|
1178
1523
|
} else if (spec.provider === 'alibaba-token-plan') {
|
|
1179
1524
|
privateProfile = true;
|
|
1180
1525
|
const qwen38 = model === 'qwen3.8-max';
|
|
@@ -1256,20 +1601,36 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
1256
1601
|
}
|
|
1257
1602
|
} else if (spec.provider === 'opencode-go') {
|
|
1258
1603
|
// Wire Responses nativa: il gateway espone `/v1/responses` e Codex-VL vi
|
|
1259
|
-
// aggiunge il path, quindi qui l'endpoint include gia' `/v1`.
|
|
1260
|
-
// model_context_window: non e' misurato, e il default del client e' piu'
|
|
1261
|
-
// onesto di una costante inventata.
|
|
1604
|
+
// aggiunge il path, quindi qui l'endpoint include gia' `/v1`.
|
|
1262
1605
|
env.OPENCODE_API_KEY = cred.value;
|
|
1263
1606
|
args.push(...codexProviderArgs('opencode_go', 'OpenCode Go', profile.endpoint, 'OPENCODE_API_KEY'));
|
|
1264
1607
|
args.push('-c', 'model_providers.opencode_go.stream_idle_timeout_ms=600000');
|
|
1608
|
+
// Codex non conosce questi modelli: senza catalogo non ha i metadati di
|
|
1609
|
+
// contesto e ricade su un default suo. Il file copre le sole coppie
|
|
1610
|
+
// misurate sulla wire Responses; il context window accompagna il modello
|
|
1611
|
+
// selezionato ed e' omesso se l'id non e' in tabella.
|
|
1612
|
+
const opencodeContext = opencodeGoContextFor(model);
|
|
1613
|
+
if (opencodeContext) {
|
|
1614
|
+
const localCatalog = path.join(__dirname, 'catalogs', 'opencode-go.json');
|
|
1615
|
+
args.push('-c', `model_catalog_json=${JSON.stringify(localCatalog)}`);
|
|
1616
|
+
args.push('-c', `model_context_window=${opencodeContext}`);
|
|
1617
|
+
}
|
|
1265
1618
|
} else if (spec.provider === 'custom') {
|
|
1266
1619
|
env[spec.envKey] = cred.value;
|
|
1267
1620
|
args.push(...codexProviderArgs(spec.providerId, spec.displayName, spec.baseUrl, spec.envKey));
|
|
1621
|
+
// D2: onora `models` (engine definition, già validato) come gli altri
|
|
1622
|
+
// provider — deriva model_catalog_json e model_context_window, così
|
|
1623
|
+
// Codex-VL non ricade sul fallback 272K. Senza `models` -> null (no regressione).
|
|
1624
|
+
const customMeta = customCatalogFor(spec, model, declaredModels, home);
|
|
1625
|
+
if (customMeta) {
|
|
1626
|
+
args.push('-c', `model_catalog_json=${JSON.stringify(customMeta.catalogPath)}`);
|
|
1627
|
+
args.push('-c', `model_context_window=${customMeta.contextWindow}`);
|
|
1628
|
+
}
|
|
1268
1629
|
}
|
|
1269
1630
|
if (model) args.push('-m', model);
|
|
1270
1631
|
} else if (spec.client === 'pi') {
|
|
1271
1632
|
if (profile.auth !== 'none' && profile.auth !== 'login' && cred.value) env[cred.envKey] = cred.value;
|
|
1272
|
-
if (spec.provider === 'custom') args.push('--extension', writePiProviderExtension(spec, home));
|
|
1633
|
+
if (spec.provider === 'custom') args.push('--extension', writePiProviderExtension(spec, home, declaredModels));
|
|
1273
1634
|
else if (profile.piExtension) args.push('--extension', writePiProviderExtension({
|
|
1274
1635
|
providerId: profile.piProvider, displayName: profile.label.replace(/^Pi · /, ''),
|
|
1275
1636
|
baseUrl: profile.piExtension.baseUrl, apiKey: profile.piExtension.apiKey,
|
|
@@ -1323,6 +1684,26 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
1323
1684
|
// child env (unset, never empty), so the runtime cannot leak credentials that
|
|
1324
1685
|
// the local store is meant to own.
|
|
1325
1686
|
applyStoreNeutralization(env, spec, profile);
|
|
1687
|
+
// D3: resolve envPassthrough AFTER the provider branches. Each declared name is
|
|
1688
|
+
// read from the credentialSources (same order as credential() 'auto': runtime
|
|
1689
|
+
// -> store -> shell -> key files -> legacy) and injected into the child env.
|
|
1690
|
+
// Per nome, mai in blocco. A name that is declared but absent from every source
|
|
1691
|
+
// fails CLOSED but not obscure: the reason names it, so a misconfigured cell
|
|
1692
|
+
// says what is missing instead of starting silent and breaking later. (vl.auth
|
|
1693
|
+
// is 'none': without this, the vl branch composes no provider env at all, yet
|
|
1694
|
+
// the runtime needs its own config vars — whose names are not fixed in the vl
|
|
1695
|
+
// binary, so the operator declares the ones their config uses.)
|
|
1696
|
+
if (spec.envPassthrough && spec.envPassthrough.length) {
|
|
1697
|
+
const sources = credentialSources(cfg, home);
|
|
1698
|
+
for (const name of spec.envPassthrough) {
|
|
1699
|
+
const value = sources.runtime[name] || sources.local[name] || sources.shell[name]
|
|
1700
|
+
|| sources.keys[name] || sources.legacy[name];
|
|
1701
|
+
if (!value) {
|
|
1702
|
+
return { ok: false, info, reason: `envPassthrough name "${name}" is not set in any credential source (environment, nexuscrew-store, ${path.basename(shellProvidersPath(cfg, home))}, key files, legacy)` };
|
|
1703
|
+
}
|
|
1704
|
+
env[name] = value;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1326
1707
|
let command = info.binary;
|
|
1327
1708
|
if (needsExplicitNode(info.binary, cfg.platform || process.platform, cfg.env || process.env)) {
|
|
1328
1709
|
command = cfg.nodeExecPath || process.execPath;
|
|
@@ -1342,6 +1723,14 @@ function publicCatalog() {
|
|
|
1342
1723
|
auth: p.auth, endpoint: p.endpoint || '', model: p.model || '', models: [...(p.models || [])],
|
|
1343
1724
|
protocols: [...(p.protocols || [p.protocol])], supportsUnsafe: !['pi', 'shell', 'vl'].includes(p.client), requiresModel: !!p.requiresModel || !!p.custom,
|
|
1344
1725
|
permissionPolicyDefault: p.client === 'claude' ? 'unsafe' : 'standard',
|
|
1726
|
+
// DEC2: solo il client claude riceve MCP gestito da NexusCrew (cellMcpArgs/
|
|
1727
|
+
// sharedMcpArgs nel ramo claude di resolveManagedEngine). Per ogni altro
|
|
1728
|
+
// client (codex, vl, kimi, pi, agy, grok) `cell.mcp` e' INERTE: la cella lo
|
|
1729
|
+
// accetta ma non ha effetto, perche' i server MCP li registra il client nel
|
|
1730
|
+
// proprio file di config nativo, non NexusCrew. La vista lo dice cosi' la
|
|
1731
|
+
// finestra puo' avvertire l'operatore NEL PUNTO in cui sceglie cell.mcp,
|
|
1732
|
+
// invece di confermare un no-op silenzioso.
|
|
1733
|
+
mcpManaged: p.client === 'claude',
|
|
1345
1734
|
rc: !!p.rc, custom: !!p.custom, default: !!p.default, notice: p.notice || '',
|
|
1346
1735
|
// 'login'/'none' non sono variabili d'ambiente: nessuna KEY section per gli
|
|
1347
1736
|
// engine che delegano l'auth al login del CLI (rappresentazione onesta).
|
|
@@ -1355,13 +1744,13 @@ module.exports = {
|
|
|
1355
1744
|
knownMcpServerNames,
|
|
1356
1745
|
CATALOG, OLLAMA_CLOUD_MODELS, OLLAMA_CONTEXT, ALIBABA_TOKEN_PLAN_MODELS,
|
|
1357
1746
|
ALIBABA_CODEX_MODELS, ALIBABA_TOKEN_PLAN_CONTEXT, ALIBABA_PI_MODELS,
|
|
1358
|
-
OPENCODE_GO_MESSAGES_MODELS, OPENCODE_GO_RESPONSES_MODELS, OPENCODE_GO_CHAT_MODELS,
|
|
1747
|
+
OPENCODE_GO_MESSAGES_MODELS, OPENCODE_GO_RESPONSES_MODELS, OPENCODE_GO_CHAT_MODELS, OPENCODE_GO_LIMITS,
|
|
1359
1748
|
OPENCODE_GO_ANTHROPIC_ROOT, OPENCODE_GO_API_BASE,
|
|
1360
1749
|
CLIENT_LABELS, normalizeManagedSpec, profileFor,
|
|
1361
1750
|
defaultDefinitions, defaultShellEngine, defaultAgyEngine, defaultKimiEngine, defaultGrokEngine, defaultVlEngine, describeManaged, describeCatalogCredential, discoverOllamaModels, resolveManagedEngine, needsExplicitNode,
|
|
1362
|
-
discoverPiModels, EXTERNAL_DISCOVERY_TIMEOUT_MS, parseEnvFile, parseProviderShellFile, findBinary, publicCatalog, writePiProviderExtension,
|
|
1363
|
-
extraModelsFrom,
|
|
1364
|
-
providerKeyPaths, parseProviderKeyFiles, credentialSources, credential,
|
|
1751
|
+
discoverPiModels, EXTERNAL_DISCOVERY_TIMEOUT_MS, parseEnvFile, parseProviderShellFile, findBinary, publicCatalog, writePiProviderExtension, customCatalogFor,
|
|
1752
|
+
extraModelsFrom, declaredModelsFor,
|
|
1753
|
+
providerKeyPaths, parseProviderKeyFiles, credentialSources, credential, CREDENTIAL_SOURCE_VALUES,
|
|
1365
1754
|
credentialEnvNeutralizeSet, applyStoreNeutralization,
|
|
1366
1755
|
ensureKimiClaudeConfig, ensureAlibabaClaudeConfig, resolveInteractiveShell,
|
|
1367
1756
|
shellLoginArgs, shellConfiguredCommandArgs, ENV_KEY_RE,
|