@mmmbuto/nexuscrew 0.8.58 → 0.9.1
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 +197 -2
- package/README.md +23 -4
- package/frontend/dist/assets/index-0vuhL1YP.css +32 -0
- package/frontend/dist/assets/index-Bu_-2-Uu.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 +54 -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 +24 -0
- package/lib/fleet/builtin.js +161 -19
- package/lib/fleet/catalogs/opencode-go.json +50 -4
- 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 +366 -48
- 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 +190 -0
- package/lib/live-host/store.js +96 -0
- package/lib/mcp/tools.js +51 -0
- package/lib/nodes/commands.js +21 -3
- package/lib/nodes/store.js +57 -0
- package/lib/nodes/tunnel.js +23 -1
- package/lib/proxy/federation.js +130 -9
- package/lib/proxy/node-proxy.js +33 -0
- package/lib/proxy/panel-auth.js +337 -0
- package/lib/proxy/panel-proxy.js +336 -0
- package/lib/server.js +252 -7
- package/lib/settings/pairing-coordinator.js +32 -0
- package/lib/settings/public-peering-routes.js +13 -1
- package/package.json +1 -1
- package/skills/aidesktop/SKILL.md +201 -0
- package/skills/aidesktop/docker/Dockerfile +21 -0
- package/skills/aidesktop/docker/custom-cont-init.d/10-cdp-relay.sh +20 -0
- package/skills/aidesktop/docker/docker-compose.example.yml +75 -0
- package/skills/crew/SKILL.md +15 -0
- package/skills/live/SKILL.md +90 -0
- package/skills/mail-assistant/SKILL.md +15 -0
- package/skills/memory/SKILL.md +15 -0
- package/skills/nexuscrew/SKILL.md +113 -0
- package/skills/nexuscrew-agent/SKILL.md +18 -0
- package/skills/vl-msa/SKILL.md +15 -0
- package/frontend/dist/assets/index-BEGNtmx2.js +0 -93
- package/frontend/dist/assets/index-CYi_lhCg.css +0 -32
- package/skills/alibaba-token-media/SKILL.md +0 -133
- package/skills/alibaba-token-media/agents/openai.yaml +0 -4
- package/skills/alibaba-token-media/references/api-contract.md +0 -97
- package/skills/alibaba-token-media/scripts/alibaba_token_media.py +0 -550
- package/skills/fill-forms/SKILL.md +0 -154
- package/skills/fill-forms/agents/openai.yaml +0 -4
- package/skills/fill-forms/references/overlay-technique.md +0 -99
- package/skills/fill-forms/requirements.txt +0 -4
- package/skills/fill-forms/scripts/dump_docx.py +0 -70
- package/skills/fill-forms/scripts/fill_docx.py +0 -207
- package/skills/fill-forms/scripts/fill_pdf.py +0 -424
- package/skills/fill-forms/scripts/inspect_pdf.py +0 -188
- package/skills/fill-forms/scripts/prepare_signature.py +0 -171
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',
|
|
@@ -206,11 +229,26 @@ const OPENCODE_GO_PI_MODELS = Object.freeze(OPENCODE_GO_CHAT_MODELS.map((id) =>
|
|
|
206
229
|
})));
|
|
207
230
|
|
|
208
231
|
const CUSTOM_KEYS = ['displayName', 'protocol', 'baseUrl', 'envKey', 'providerId'];
|
|
209
|
-
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;
|
|
210
236
|
// Explicit credential source policy. Default 'auto' preserves the legacy
|
|
211
237
|
// resolution order (runtime -> store -> shell -> key files -> legacy) so a
|
|
212
238
|
// pre-WP1 fleet.json migrates no-op: no existing cell changes resolution.
|
|
213
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
|
+
]);
|
|
214
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' });
|
|
215
253
|
const PROVIDER_ID_RE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
216
254
|
|
|
@@ -237,7 +275,12 @@ const CATALOG = Object.freeze([
|
|
|
237
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 },
|
|
238
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' },
|
|
239
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' },
|
|
240
|
-
|
|
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 },
|
|
241
284
|
// OpenCode Go su Claude parla Anthropic Messages, e la wire accetta SOLO
|
|
242
285
|
// `x-api-key`: con `Authorization: Bearer` risponde 401 AuthError. Per questo
|
|
243
286
|
// l'endpoint e' la root senza `/v1` (il client aggiunge `/v1/messages`) e il
|
|
@@ -317,7 +360,7 @@ const CATALOG = Object.freeze([
|
|
|
317
360
|
{ id: 'pi.github-copilot', client: 'pi', provider: 'github-copilot', label: 'GitHub Copilot', auth: 'login', protocol: 'pi_native', piProvider: 'github-copilot', core: true },
|
|
318
361
|
{ id: 'pi.deepseek', client: 'pi', provider: 'deepseek', label: 'DeepSeek', auth: 'DEEPSEEK_API_KEY', protocol: 'pi_native', piProvider: 'deepseek', core: true },
|
|
319
362
|
// Provider Pi NON core: restano fuori dal catalogo UI (publicCatalog filtra
|
|
320
|
-
// core/default/custom), in attesa di decisione
|
|
363
|
+
// core/default/custom), in attesa di una decisione di progetto. Vengono risolti solo da
|
|
321
364
|
// configurazione esistente via profileFor. Etichette senza prefisso "Pi · ".
|
|
322
365
|
{ id: 'pi.fireworks', client: 'pi', provider: 'fireworks', label: 'Fireworks AI', auth: 'FIREWORKS_API_KEY', protocol: 'pi_native', piProvider: 'fireworks' },
|
|
323
366
|
{ id: 'pi.huggingface', client: 'pi', provider: 'huggingface', label: 'Hugging Face', auth: 'HF_TOKEN', protocol: 'pi_native', piProvider: 'huggingface' },
|
|
@@ -356,8 +399,8 @@ const CATALOG = Object.freeze([
|
|
|
356
399
|
// --- Sezione legacy --------------------------------------------------------
|
|
357
400
|
// Compatibilita' sola lettura/launch per configurazioni 0.8.0: mai nel catalogo
|
|
358
401
|
// UI (publicCatalog filtra `legacy`). Risolti solo da profileFor/normalizeManagedSpec.
|
|
359
|
-
{ 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.
|
|
360
|
-
{ 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 },
|
|
361
404
|
]);
|
|
362
405
|
|
|
363
406
|
function profileFor(client, provider, credentialProfile) {
|
|
@@ -428,6 +471,27 @@ function normalizeManagedSpec(value, { extraModels = null } = {}) {
|
|
|
428
471
|
if (!model || !(profile.protocols || [profile.protocol]).includes(protocol)) return null;
|
|
429
472
|
Object.assign(out, { displayName, baseUrl, envKey, protocol, providerId });
|
|
430
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
|
+
}
|
|
431
495
|
return out;
|
|
432
496
|
}
|
|
433
497
|
|
|
@@ -535,7 +599,17 @@ function safeAllowedRoots(roots = []) {
|
|
|
535
599
|
return out;
|
|
536
600
|
}
|
|
537
601
|
|
|
538
|
-
|
|
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;
|
|
539
613
|
try {
|
|
540
614
|
const lst = fs.lstatSync(file);
|
|
541
615
|
let target = file;
|
|
@@ -549,16 +623,23 @@ function parseEnvFile(file, opts = {}) {
|
|
|
549
623
|
if (!st.isFile() || st.isSymbolicLink() || (st.mode & 0o077) || st.size > 256 * 1024) return {};
|
|
550
624
|
if (typeof process.getuid === 'function' && st.uid !== process.getuid()) return {};
|
|
551
625
|
return parseAssignments(fs.readFileSync(target, 'utf8'));
|
|
552
|
-
} catch (
|
|
626
|
+
} catch (e) {
|
|
627
|
+
if (blocked && e.code !== 'ENOENT') blocked.push({ path: file, code: e.code || e.constructor.name });
|
|
628
|
+
return {};
|
|
629
|
+
}
|
|
553
630
|
}
|
|
554
631
|
|
|
555
|
-
function parseProviderShellFile(file) {
|
|
632
|
+
function parseProviderShellFile(file, out) {
|
|
633
|
+
const blocked = Array.isArray(out && out.blocked) ? out.blocked : null;
|
|
556
634
|
try {
|
|
557
635
|
const st = fs.lstatSync(file);
|
|
558
636
|
if (!st.isFile() || st.isSymbolicLink() || (st.mode & 0o022) || st.size > 256 * 1024) return {};
|
|
559
637
|
if (typeof process.getuid === 'function' && st.uid !== process.getuid()) return {};
|
|
560
638
|
return parseAssignments(fs.readFileSync(file, 'utf8'));
|
|
561
|
-
} catch (
|
|
639
|
+
} catch (e) {
|
|
640
|
+
if (blocked && e.code !== 'ENOENT') blocked.push({ path: file, code: e.code || e.constructor.name });
|
|
641
|
+
return {};
|
|
642
|
+
}
|
|
562
643
|
}
|
|
563
644
|
|
|
564
645
|
function binaryCandidates(client, home) {
|
|
@@ -575,14 +656,28 @@ function binaryCandidates(client, home) {
|
|
|
575
656
|
].filter(Boolean))];
|
|
576
657
|
}
|
|
577
658
|
|
|
578
|
-
|
|
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;
|
|
579
669
|
for (const candidate of binaryCandidates(client, home)) {
|
|
580
670
|
try {
|
|
581
671
|
const real = fs.realpathSync(candidate); const st = fs.lstatSync(real);
|
|
582
672
|
if (!st.isFile() || !(st.mode & 0o100) || (st.mode & 0o002)) continue;
|
|
583
673
|
if (typeof process.getuid === 'function' && st.uid !== process.getuid() && st.uid !== 0) continue;
|
|
584
674
|
return real;
|
|
585
|
-
} 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
|
+
}
|
|
586
681
|
}
|
|
587
682
|
return null;
|
|
588
683
|
}
|
|
@@ -590,7 +685,7 @@ function findBinary(client, home) {
|
|
|
590
685
|
// Resolve a device-local interactive shell without persisting a path in
|
|
591
686
|
// fleet.json. Candidates are ordered and fail closed. Symlinks are resolved
|
|
592
687
|
// first, then the existing command trust policy is applied to the real file.
|
|
593
|
-
function resolveInteractiveShell(cfg = {}) {
|
|
688
|
+
function resolveInteractiveShell(cfg = {}, out) {
|
|
594
689
|
const env = cfg.env || process.env;
|
|
595
690
|
const platform = cfg.platform || process.platform;
|
|
596
691
|
const termux = termuxRuntimePaths(env, { platform, home: cfg.home });
|
|
@@ -603,11 +698,19 @@ function resolveInteractiveShell(cfg = {}) {
|
|
|
603
698
|
candidates.push('/bin/bash', '/bin/sh');
|
|
604
699
|
const validate = cfg.validateCommandTrust
|
|
605
700
|
|| ((command) => require('./definitions.js').validateCommandTrust(command));
|
|
701
|
+
const blocked = Array.isArray(out && out.blocked) ? out.blocked : null;
|
|
606
702
|
for (const candidate of [...new Set(candidates)]) {
|
|
607
703
|
try {
|
|
608
704
|
const real = fs.realpathSync(candidate);
|
|
609
705
|
if (validate(real).ok) return real;
|
|
610
|
-
} 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
|
+
}
|
|
611
714
|
}
|
|
612
715
|
return null;
|
|
613
716
|
}
|
|
@@ -672,34 +775,40 @@ function providerKeyPaths(cfg, home) {
|
|
|
672
775
|
return [...new Set(paths.filter((file) => typeof file === 'string' && file))];
|
|
673
776
|
}
|
|
674
777
|
|
|
675
|
-
function parseProviderKeyFiles(cfg, home) {
|
|
778
|
+
function parseProviderKeyFiles(cfg, home, out) {
|
|
676
779
|
const values = {};
|
|
677
780
|
// Match providers.zsh ordering: a later secure file may intentionally
|
|
678
781
|
// override the canonical ai.env value. Files remain data-only and must be
|
|
679
782
|
// private regular files owned by the NexusCrew user.
|
|
680
783
|
const files = providerKeyPaths(cfg, home);
|
|
681
784
|
const roots = [...new Set(files.map((file) => path.dirname(path.resolve(file))))];
|
|
682
|
-
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));
|
|
683
786
|
return values;
|
|
684
787
|
}
|
|
685
788
|
|
|
686
|
-
|
|
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 } = {}) {
|
|
687
796
|
let local = {};
|
|
688
797
|
try { local = readCredentialStore(cfg, home); } catch (_) { /* unsafe/corrupt store is ignored, never trusted */ }
|
|
689
798
|
return {
|
|
690
799
|
runtime: cfg.env || process.env,
|
|
691
800
|
local,
|
|
692
|
-
shell: parseProviderShellFile(shellProvidersPath(cfg, home)),
|
|
693
|
-
keys: parseProviderKeyFiles(cfg, home),
|
|
694
|
-
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),
|
|
695
804
|
};
|
|
696
805
|
}
|
|
697
806
|
|
|
698
|
-
function credential(profile, spec, cfg, home) {
|
|
807
|
+
function credential(profile, spec, cfg, home, out) {
|
|
699
808
|
if (profile.auth === 'login' || profile.auth === 'none') return { envKey: profile.auth, value: '', source: profile.auth };
|
|
700
809
|
const envKey = profile.auth === 'dynamic' ? spec.envKey : profile.auth;
|
|
701
810
|
const policy = spec && CREDENTIAL_SOURCES.includes(spec.credentialSourcePolicy) ? spec.credentialSourcePolicy : 'auto';
|
|
702
|
-
const sources = credentialSources(cfg, home);
|
|
811
|
+
const sources = credentialSources(cfg, home, out, { trackLegacy: !!profile.legacySecrets });
|
|
703
812
|
// The fixed shell file is already the user's environment source. Values are
|
|
704
813
|
// consumed only in memory and passed to the selected child; never persisted
|
|
705
814
|
// in fleet.json, service files, API responses or logs.
|
|
@@ -719,7 +828,15 @@ function credential(profile, spec, cfg, home) {
|
|
|
719
828
|
if (profile.legacySecrets && sources.legacy[envKey]) {
|
|
720
829
|
return { envKey, value: sources.legacy[envKey], source: 'compatibility' };
|
|
721
830
|
}
|
|
722
|
-
|
|
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' };
|
|
723
840
|
}
|
|
724
841
|
|
|
725
842
|
// The profile's "owned" env set. When the credential source is the local store,
|
|
@@ -839,22 +956,54 @@ async function discoverPiModels(opts = {}) {
|
|
|
839
956
|
function describeManaged(spec, cfg = {}) {
|
|
840
957
|
const extraModels = cfg.extraModels || null;
|
|
841
958
|
const normalized = normalizeManagedSpec(spec, { extraModels });
|
|
842
|
-
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
|
+
}
|
|
843
975
|
const home = cfg.home || require('node:os').homedir();
|
|
844
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 = [];
|
|
845
985
|
const binary = normalized.client === 'shell'
|
|
846
|
-
? resolveInteractiveShell({ ...cfg, home })
|
|
847
|
-
: findBinary(normalized.client, home);
|
|
848
|
-
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 });
|
|
849
989
|
// Pi can resolve credentials from its own documented /login auth store. Do
|
|
850
990
|
// not inspect or copy that store; delegate native-provider auth to Pi.
|
|
851
991
|
const delegatedPiAuth = profile.client === 'pi' && profile.provider !== 'custom'
|
|
852
992
|
&& profile.delegatePiAuth !== false;
|
|
853
993
|
const authConfigured = delegatedPiAuth || profile.auth === 'login' || profile.auth === 'none' || !!cred.value;
|
|
854
994
|
let configured = !!binary && authConfigured;
|
|
855
|
-
let reason
|
|
856
|
-
|
|
857
|
-
|
|
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
|
+
}
|
|
858
1007
|
// Agy e grok sono client primari supportati solo su Linux/macOS non-Termux.
|
|
859
1008
|
// Rilevazione Termux via termuxRuntimePaths (non solo process.platform): un
|
|
860
1009
|
// Node che riporta 'linux' sotto proot/Termux viene comunque intercettato.
|
|
@@ -872,7 +1021,11 @@ function describeManaged(spec, cfg = {}) {
|
|
|
872
1021
|
permissionPolicy: normalized.permissionPolicy, protocol: normalized.protocol || profile.protocol,
|
|
873
1022
|
endpoint: normalized.baseUrl || profile.endpoint || '', auth: cred.envKey, authConfigured,
|
|
874
1023
|
credentialSourcePolicy: normalized.credentialSourcePolicy || 'auto',
|
|
875
|
-
|
|
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,
|
|
876
1029
|
configured, models: [...(profile.models || [])], defaultModel: profile.model || '',
|
|
877
1030
|
binary: binary || '', displayName: normalized.displayName || profile.label,
|
|
878
1031
|
reason,
|
|
@@ -887,11 +1040,14 @@ function describeCatalogCredential(client, provider, credentialProfile = '', cfg
|
|
|
887
1040
|
if (!profile || profile.auth === 'dynamic' || profile.auth === 'login' || profile.auth === 'none'
|
|
888
1041
|
|| !ENV_KEY_RE.test(profile.auth || '')) return null;
|
|
889
1042
|
const home = cfg.home || require('node:os').homedir();
|
|
890
|
-
const cred = credential(profile, {}, cfg, home);
|
|
1043
|
+
const cred = credential(profile, {}, cfg, home, { blocked: [] });
|
|
891
1044
|
return {
|
|
892
1045
|
envKey: cred.envKey,
|
|
893
1046
|
authConfigured: !!cred.value,
|
|
894
|
-
|
|
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,
|
|
895
1051
|
};
|
|
896
1052
|
}
|
|
897
1053
|
|
|
@@ -1091,7 +1247,41 @@ function ensurePrivateClaudeConfig(home, profileId, label, penguinMode) {
|
|
|
1091
1247
|
return configDir;
|
|
1092
1248
|
}
|
|
1093
1249
|
|
|
1094
|
-
|
|
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) {
|
|
1095
1285
|
const dir = path.join(home, '.nexuscrew', 'pi-providers');
|
|
1096
1286
|
try {
|
|
1097
1287
|
const st = fs.lstatSync(dir);
|
|
@@ -1113,7 +1303,8 @@ function writePiProviderExtension(spec, home) {
|
|
|
1113
1303
|
apiKey: spec.apiKey || `$${spec.envKey}`,
|
|
1114
1304
|
authHeader: true,
|
|
1115
1305
|
api: spec.protocol,
|
|
1116
|
-
models: Array.isArray(
|
|
1306
|
+
models: (Array.isArray(declaredModels) && declaredModels.length ? declaredModels.map(toPiModelConfig)
|
|
1307
|
+
: (Array.isArray(spec.models) && spec.models.length ? spec.models : null)) || [{
|
|
1117
1308
|
id: spec.model, name: spec.model, reasoning: false, input: ['text'],
|
|
1118
1309
|
contextWindow: 128000, maxTokens: 16384,
|
|
1119
1310
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
@@ -1132,6 +1323,87 @@ function writePiProviderExtension(spec, home) {
|
|
|
1132
1323
|
return target;
|
|
1133
1324
|
}
|
|
1134
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
|
+
|
|
1135
1407
|
function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
1136
1408
|
// `extraModels` DEVE arrivare fin qui. Threadarlo al chiamante e alla vista
|
|
1137
1409
|
// non basta: la normalizzazione che decide se la cella PARTE e' questa, e
|
|
@@ -1144,6 +1416,12 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
1144
1416
|
if (!spec || !info.configured) return { ok: false, reason: info.reason, info };
|
|
1145
1417
|
const home = cfg.home || require('node:os').homedir();
|
|
1146
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);
|
|
1147
1425
|
const cred = credential(profile, spec, cfg, home);
|
|
1148
1426
|
// L'override PER-CELLA va canonicalizzato come lo spec: `normalizeManagedSpec`
|
|
1149
1427
|
// applica l'alias a `spec.model`, ma `cell.model` lo scavalca DOPO e senza
|
|
@@ -1209,7 +1487,11 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
1209
1487
|
CLAUDE_CODE_MAX_CONTEXT_TOKENS: contextWindow,
|
|
1210
1488
|
API_TIMEOUT_MS: '3000000',
|
|
1211
1489
|
});
|
|
1212
|
-
|
|
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')) {
|
|
1213
1495
|
env.CLAUDE_CODE_EFFORT_LEVEL = 'max';
|
|
1214
1496
|
env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT = '1';
|
|
1215
1497
|
}
|
|
@@ -1336,11 +1618,19 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
1336
1618
|
} else if (spec.provider === 'custom') {
|
|
1337
1619
|
env[spec.envKey] = cred.value;
|
|
1338
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
|
+
}
|
|
1339
1629
|
}
|
|
1340
1630
|
if (model) args.push('-m', model);
|
|
1341
1631
|
} else if (spec.client === 'pi') {
|
|
1342
1632
|
if (profile.auth !== 'none' && profile.auth !== 'login' && cred.value) env[cred.envKey] = cred.value;
|
|
1343
|
-
if (spec.provider === 'custom') args.push('--extension', writePiProviderExtension(spec, home));
|
|
1633
|
+
if (spec.provider === 'custom') args.push('--extension', writePiProviderExtension(spec, home, declaredModels));
|
|
1344
1634
|
else if (profile.piExtension) args.push('--extension', writePiProviderExtension({
|
|
1345
1635
|
providerId: profile.piProvider, displayName: profile.label.replace(/^Pi · /, ''),
|
|
1346
1636
|
baseUrl: profile.piExtension.baseUrl, apiKey: profile.piExtension.apiKey,
|
|
@@ -1394,6 +1684,26 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
1394
1684
|
// child env (unset, never empty), so the runtime cannot leak credentials that
|
|
1395
1685
|
// the local store is meant to own.
|
|
1396
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
|
+
}
|
|
1397
1707
|
let command = info.binary;
|
|
1398
1708
|
if (needsExplicitNode(info.binary, cfg.platform || process.platform, cfg.env || process.env)) {
|
|
1399
1709
|
command = cfg.nodeExecPath || process.execPath;
|
|
@@ -1413,6 +1723,14 @@ function publicCatalog() {
|
|
|
1413
1723
|
auth: p.auth, endpoint: p.endpoint || '', model: p.model || '', models: [...(p.models || [])],
|
|
1414
1724
|
protocols: [...(p.protocols || [p.protocol])], supportsUnsafe: !['pi', 'shell', 'vl'].includes(p.client), requiresModel: !!p.requiresModel || !!p.custom,
|
|
1415
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',
|
|
1416
1734
|
rc: !!p.rc, custom: !!p.custom, default: !!p.default, notice: p.notice || '',
|
|
1417
1735
|
// 'login'/'none' non sono variabili d'ambiente: nessuna KEY section per gli
|
|
1418
1736
|
// engine che delegano l'auth al login del CLI (rappresentazione onesta).
|
|
@@ -1426,13 +1744,13 @@ module.exports = {
|
|
|
1426
1744
|
knownMcpServerNames,
|
|
1427
1745
|
CATALOG, OLLAMA_CLOUD_MODELS, OLLAMA_CONTEXT, ALIBABA_TOKEN_PLAN_MODELS,
|
|
1428
1746
|
ALIBABA_CODEX_MODELS, ALIBABA_TOKEN_PLAN_CONTEXT, ALIBABA_PI_MODELS,
|
|
1429
|
-
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,
|
|
1430
1748
|
OPENCODE_GO_ANTHROPIC_ROOT, OPENCODE_GO_API_BASE,
|
|
1431
1749
|
CLIENT_LABELS, normalizeManagedSpec, profileFor,
|
|
1432
1750
|
defaultDefinitions, defaultShellEngine, defaultAgyEngine, defaultKimiEngine, defaultGrokEngine, defaultVlEngine, describeManaged, describeCatalogCredential, discoverOllamaModels, resolveManagedEngine, needsExplicitNode,
|
|
1433
|
-
discoverPiModels, EXTERNAL_DISCOVERY_TIMEOUT_MS, parseEnvFile, parseProviderShellFile, findBinary, publicCatalog, writePiProviderExtension,
|
|
1434
|
-
extraModelsFrom,
|
|
1435
|
-
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,
|
|
1436
1754
|
credentialEnvNeutralizeSet, applyStoreNeutralization,
|
|
1437
1755
|
ensureKimiClaudeConfig, ensureAlibabaClaudeConfig, resolveInteractiveShell,
|
|
1438
1756
|
shellLoginArgs, shellConfiguredCommandArgs, ENV_KEY_RE,
|