@mmmbuto/nexuscrew 0.9.14 → 0.9.16
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/frontend/dist/assets/index-CprIQlC5.css +32 -0
- package/frontend/dist/assets/index-CwsGpQwK.js +93 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/audio/receipt.js +1 -1
- package/lib/cells/routes.js +26 -9
- package/lib/config.js +12 -0
- package/lib/diagnostics/store.js +4 -1
- package/lib/fleet/builtin.js +53 -17
- package/lib/fleet/catalogs/opencode-go.json +3 -3
- package/lib/fleet/definitions.js +29 -10
- package/lib/fleet/managed.js +86 -15
- package/lib/live-host/bridge.js +156 -8
- package/lib/live-host/routes.js +10 -2
- package/lib/mcp/server.js +28 -6
- package/lib/mcp/tools.js +24 -6
- package/lib/nodes/health.js +43 -9
- package/lib/server.js +23 -3
- package/lib/ws/bridge.js +243 -24
- package/lib/ws/drop-counter.js +37 -0
- package/package.json +1 -1
- package/skills/nexuscrew/SKILL.md +1 -1
- package/frontend/dist/assets/index-9jCxHZwQ.js +0 -93
- package/frontend/dist/assets/index-keXh4CAm.css +0 -32
package/frontend/dist/index.html
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
<meta name="apple-mobile-web-app-title" content="NexusCrew" />
|
|
12
12
|
<link rel="manifest" href="/manifest.json" />
|
|
13
13
|
<title>NexusCrew</title>
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-CwsGpQwK.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CprIQlC5.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="root"></div>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.9.
|
|
1
|
+
{"version":"0.9.16"}
|
package/lib/audio/receipt.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// lib/audio/receipt.js — receipt degli enunciati, bounded e ridotto all'osso.
|
|
3
3
|
//
|
|
4
4
|
// Scope del chiamante = NODO + CELLA. Non la sola cella: due nodi possono avere
|
|
5
|
-
// celle omonime (`
|
|
5
|
+
// celle omonime (`Alpha` esiste su piu' installazioni), e una chiave basata solo
|
|
6
6
|
// sul nome permetterebbe a un nodo di leggere o sovrascrivere i receipt di una
|
|
7
7
|
// cella altrui con lo stesso nome. La chiave e' costruita qui, dal server, mai
|
|
8
8
|
// accettata dal client.
|
package/lib/cells/routes.js
CHANGED
|
@@ -68,7 +68,7 @@ function validIdentity(value) {
|
|
|
68
68
|
&& isValidSession(value.tmuxSession);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
function cellsRoutes({ fleetP, instanceId, submit, readonly = () => false, now = () => Date.now() }) {
|
|
71
|
+
function cellsRoutes({ fleetP, instanceId, submit, readonly = () => false, now = () => Date.now(), diagnostics = null }) {
|
|
72
72
|
const r = express.Router();
|
|
73
73
|
|
|
74
74
|
async function status() {
|
|
@@ -96,31 +96,47 @@ function cellsRoutes({ fleetP, instanceId, submit, readonly = () => false, now =
|
|
|
96
96
|
r.post('/send', express.json({ limit: '16kb' }), async (req, res) => {
|
|
97
97
|
if (readonly()) return res.status(403).json({ error: 'READONLY: invio cella bloccato' });
|
|
98
98
|
const body = req.body || {};
|
|
99
|
+
// Observability (2026-08-28): chi manda, a chi, con quale id e come finisce.
|
|
100
|
+
// SOLO metadati: il testo del messaggio non tocca mai la diagnostica.
|
|
101
|
+
const logSend = (level, event, reason) => {
|
|
102
|
+
if (!diagnostics) return;
|
|
103
|
+
try {
|
|
104
|
+
diagnostics.record(level, 'cell-msg', event, reason
|
|
105
|
+
? `Invio cella: ${reason}`
|
|
106
|
+
: 'Invio cella consegnato alla sessione target', {
|
|
107
|
+
fromCell: body.from && body.from.cell,
|
|
108
|
+
toCell: body.to && body.to.cell,
|
|
109
|
+
msgId: typeof body.id === 'string' ? body.id : undefined,
|
|
110
|
+
reason: reason ? String(reason).slice(0, 48) : undefined,
|
|
111
|
+
});
|
|
112
|
+
} catch (_) {}
|
|
113
|
+
};
|
|
114
|
+
const reject = (code, reason) => { logSend('warn', 'CELL_MESSAGE_REJECTED', reason); return res.status(code).json({ error: reason }); };
|
|
99
115
|
const keys = Object.keys(body);
|
|
100
116
|
if (keys.some((key) => !['id', 'from', 'to', 'message'].includes(key))
|
|
101
117
|
|| !MESSAGE_ID_RE.test(String(body.id || ''))
|
|
102
118
|
|| !validIdentity(body.from) || !validIdentity(body.to)
|
|
103
119
|
|| !submitTextOk(body.message)) {
|
|
104
|
-
return
|
|
120
|
+
return reject(400, 'messaggio cella non valido');
|
|
105
121
|
}
|
|
106
122
|
const localId = instanceId();
|
|
107
123
|
if (!NODE_ID_RE.test(String(localId || '')) || body.to.instanceId !== localId) {
|
|
108
|
-
return
|
|
124
|
+
return reject(409, 'destinazione non appartiene a questo nodo');
|
|
109
125
|
}
|
|
110
126
|
const visited = parseVisited(req);
|
|
111
127
|
if (visited === null || (visited.length && (visited.at(-1) !== localId
|
|
112
128
|
|| body.from.instanceId !== visited[0]))) {
|
|
113
|
-
return
|
|
129
|
+
return reject(403, 'identita mittente non verificata');
|
|
114
130
|
}
|
|
115
131
|
if (!visited.length && body.from.instanceId !== localId) {
|
|
116
|
-
return
|
|
132
|
+
return reject(403, 'mittente remoto senza route autenticata');
|
|
117
133
|
}
|
|
118
134
|
try {
|
|
119
135
|
const cells = publicCells(await status(), localId, now());
|
|
120
136
|
const target = cells.find((cell) => cell.cell === body.to.cell
|
|
121
137
|
&& cell.tmuxSession === body.to.tmuxSession);
|
|
122
|
-
if (!target) return
|
|
123
|
-
if (!target.canReceive) return
|
|
138
|
+
if (!target) return reject(404, 'cella destinataria sconosciuta');
|
|
139
|
+
if (!target.canReceive) return reject(409, 'cella destinataria non attiva');
|
|
124
140
|
const label = `${body.from.cell}@${body.from.instanceId.slice(0, 8)}`;
|
|
125
141
|
// End on printable text even when the source message ends in a newline:
|
|
126
142
|
// Pi may auto-submit a bracketed paste that ends with LF. NexusCrew owns
|
|
@@ -128,8 +144,9 @@ function cellsRoutes({ fleetP, instanceId, submit, readonly = () => false, now =
|
|
|
128
144
|
const envelope = `[NexusCrew message ${body.id} from ${label}]\n${body.message}\n[End NexusCrew message]`;
|
|
129
145
|
const outcome = await submit(target.tmuxSession, envelope, { engine: target.engine });
|
|
130
146
|
if (!outcome || outcome.submitted !== true) {
|
|
131
|
-
return
|
|
147
|
+
return reject(409, outcome?.reason || 'consegna non riuscita');
|
|
132
148
|
}
|
|
149
|
+
logSend('notice', 'CELL_MESSAGE_SENT');
|
|
133
150
|
const at = now();
|
|
134
151
|
return res.json({
|
|
135
152
|
id: body.id,
|
|
@@ -138,7 +155,7 @@ function cellsRoutes({ fleetP, instanceId, submit, readonly = () => false, now =
|
|
|
138
155
|
to: { instanceId: localId, cell: target.cell, tmuxSession: target.tmuxSession },
|
|
139
156
|
note: 'submitted conferma solo paste+Enter nel TUI, non elaborazione o completamento',
|
|
140
157
|
});
|
|
141
|
-
} catch (e) { return
|
|
158
|
+
} catch (e) { return reject(500, String(e.message || e).slice(0, 96)); }
|
|
142
159
|
});
|
|
143
160
|
|
|
144
161
|
r.use((err, _req, res, _next) => {
|
package/lib/config.js
CHANGED
|
@@ -67,6 +67,14 @@ function baseDefaults() {
|
|
|
67
67
|
// MC1.5: limite dichiarato per OGNI fase del ponte (GET designazione e
|
|
68
68
|
// sessione sul socket). Oltre questo la Live parte senza puntamento.
|
|
69
69
|
liveBridgeTimeoutMs: 1500,
|
|
70
|
+
// PTY grace is finite and configurable for mobile handovers. The caps keep
|
|
71
|
+
// a small host from retaining an unbounded number of disconnected sessions.
|
|
72
|
+
ptyGraceMs: 30000,
|
|
73
|
+
ptyGraceMaxSessions: 8,
|
|
74
|
+
ptyGraceMaxMemoryBytes: 8 * 1024,
|
|
75
|
+
// Un singolo timeout del probe può essere jitter mobile: servono tre
|
|
76
|
+
// fallimenti consecutivi prima di dichiarare il peer irraggiungibile.
|
|
77
|
+
nodeHealthFailureThreshold: 3,
|
|
70
78
|
};
|
|
71
79
|
}
|
|
72
80
|
|
|
@@ -119,6 +127,10 @@ function envOverrides() {
|
|
|
119
127
|
}
|
|
120
128
|
if (process.env.NEXUSCREW_LIVE_BRIDGE_SOCKET) e.liveBridgeSocketPath = process.env.NEXUSCREW_LIVE_BRIDGE_SOCKET;
|
|
121
129
|
if (process.env.NEXUSCREW_LIVE_BRIDGE_TIMEOUT_MS) e.liveBridgeTimeoutMs = Number(process.env.NEXUSCREW_LIVE_BRIDGE_TIMEOUT_MS);
|
|
130
|
+
if (process.env.NEXUSCREW_PTY_GRACE_MS) e.ptyGraceMs = Number(process.env.NEXUSCREW_PTY_GRACE_MS);
|
|
131
|
+
if (process.env.NEXUSCREW_PTY_GRACE_MAX_SESSIONS) e.ptyGraceMaxSessions = Number(process.env.NEXUSCREW_PTY_GRACE_MAX_SESSIONS);
|
|
132
|
+
if (process.env.NEXUSCREW_PTY_GRACE_MAX_MEMORY_BYTES) e.ptyGraceMaxMemoryBytes = Number(process.env.NEXUSCREW_PTY_GRACE_MAX_MEMORY_BYTES);
|
|
133
|
+
if (process.env.NEXUSCREW_NODE_HEALTH_FAILURE_THRESHOLD) e.nodeHealthFailureThreshold = Number(process.env.NEXUSCREW_NODE_HEALTH_FAILURE_THRESHOLD);
|
|
122
134
|
return e;
|
|
123
135
|
}
|
|
124
136
|
|
package/lib/diagnostics/store.js
CHANGED
|
@@ -4,10 +4,13 @@ const DEFAULT_MAX_RECORDS = 500;
|
|
|
4
4
|
const DEFAULT_MAX_BYTES = 256 * 1024;
|
|
5
5
|
const DEFAULT_MAX_ENTRY_BYTES = 4096;
|
|
6
6
|
const ALLOWED_DURATIONS = new Set([300, 900, 1800, 3600]);
|
|
7
|
-
const LEVELS = new Set(['debug', 'info', 'warn', 'error']);
|
|
7
|
+
const LEVELS = new Set(['debug', 'info', 'notice', 'warn', 'error']);
|
|
8
8
|
const META_KEYS = new Set([
|
|
9
9
|
'port', 'platform', 'reason', 'durationSeconds', 'count', 'errno', 'client',
|
|
10
10
|
'cell', 'engine', 'action', 'state', 'transport', 'phase', 'version', 'status', 'node', 'code',
|
|
11
|
+
// Observability (2026-08-28): metadati di routing e chiusure — identita di
|
|
12
|
+
// cella e id di messaggio, MAI contenuto (DENIED_KEY resta la seconda rete).
|
|
13
|
+
'fromCell', 'toCell', 'msgId', 'sessionId', 'durationMs', 'gapMs', 'closeCode', 'exitCode', 'drops',
|
|
11
14
|
]);
|
|
12
15
|
const DENIED_KEY = /(authorization|cookie|token|secret|credential|password|prompt|terminal|argv|command|env|content|payload|file|path|endpoint|url)/i;
|
|
13
16
|
|
package/lib/fleet/builtin.js
CHANGED
|
@@ -84,23 +84,32 @@ function draftFrom(defs) {
|
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
function warnEngineCap(log, engineId, count) {
|
|
88
|
+
const emit = typeof log === 'function' ? log : console.warn;
|
|
89
|
+
emit(`WARN fleet backfill: engine ${engineId} non aggiunto: ${count} engine dichiarati, cap ${CAPS.MAX_ENGINES} raggiunto; riduci gli engine prima di riprovare`);
|
|
90
|
+
}
|
|
91
|
+
|
|
87
92
|
// Upgrade locale, idempotente e non distruttivo: installazioni gia' esistenti
|
|
88
93
|
// ricevono l'engine standard Shell senza riscrivere celle o sostituire un id
|
|
89
94
|
// scelto dall'utente. Se lo store e' pieno o la scrittura non e' possibile, il
|
|
90
95
|
// bootstrap resta utilizzabile con le definizioni precedenti.
|
|
91
96
|
function backfillShellEngine(defsPath, defs, log) {
|
|
92
97
|
if (!defs) return defs;
|
|
98
|
+
const emit = typeof log === 'function' ? log : console.warn;
|
|
93
99
|
// Le condizioni si valutano su cio' che si legge DENTRO il lock, non
|
|
94
100
|
// sullo stato che avevamo in mano: e' la differenza fra decidere sul
|
|
95
101
|
// presente e decidere su una fotografia.
|
|
96
102
|
const esito = aggiornaDefinizioni(defsPath, (dentro) => {
|
|
97
103
|
if (dentro.engines.some((engine) => engine.managed?.client === 'shell')) return null;
|
|
98
104
|
if (dentro.engines.some((engine) => engine.id === 'shell.local')) return null;
|
|
99
|
-
if (dentro.engines.length >= CAPS.MAX_ENGINES)
|
|
105
|
+
if (dentro.engines.length >= CAPS.MAX_ENGINES) {
|
|
106
|
+
warnEngineCap(emit, 'shell.local', dentro.engines.length);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
100
109
|
const draft = draftFrom(dentro);
|
|
101
110
|
draft.engines.push(defaultShellEngine());
|
|
102
111
|
return draft;
|
|
103
|
-
}, { log });
|
|
112
|
+
}, { log: emit });
|
|
104
113
|
return esito || defs;
|
|
105
114
|
}
|
|
106
115
|
|
|
@@ -115,6 +124,7 @@ function backfillShellEngine(defsPath, defs, log) {
|
|
|
115
124
|
// avviene solo nel field gate esplicito del coordinator).
|
|
116
125
|
function backfillAgyEngine(defsPath, defs, cfg = {}) {
|
|
117
126
|
if (!defs) return defs;
|
|
127
|
+
const emit = typeof cfg.log === 'function' ? cfg.log : console.warn;
|
|
118
128
|
const platform = cfg.platform || process.platform;
|
|
119
129
|
const termux = platform === 'android'
|
|
120
130
|
|| termuxRuntimePaths(cfg.env || process.env, { platform, home: cfg.home }) !== null;
|
|
@@ -124,11 +134,14 @@ function backfillAgyEngine(defsPath, defs, cfg = {}) {
|
|
|
124
134
|
const esito = aggiornaDefinizioni(defsPath, (dentro) => {
|
|
125
135
|
if (dentro.engines.some((engine) => engine.managed?.client === 'agy')) return null;
|
|
126
136
|
if (dentro.engines.some((engine) => engine.id === 'agy.native')) return null;
|
|
127
|
-
if (dentro.engines.length >= CAPS.MAX_ENGINES)
|
|
137
|
+
if (dentro.engines.length >= CAPS.MAX_ENGINES) {
|
|
138
|
+
warnEngineCap(emit, 'agy.native', dentro.engines.length);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
128
141
|
const draft = draftFrom(dentro);
|
|
129
142
|
draft.engines.push(defaultAgyEngine());
|
|
130
143
|
return draft;
|
|
131
|
-
}, { log:
|
|
144
|
+
}, { log: emit });
|
|
132
145
|
return esito || defs;
|
|
133
146
|
}
|
|
134
147
|
|
|
@@ -238,16 +251,20 @@ function defaultDesktopEngine() {
|
|
|
238
251
|
// — e' il posto dove qualcuno sa gia' che il container esiste.
|
|
239
252
|
function backfillDesktopEngine(defsPath, defs, log) {
|
|
240
253
|
if (!defs) return defs;
|
|
254
|
+
const emit = typeof log === 'function' ? log : console.warn;
|
|
241
255
|
if (defs.engines.some((engine) => engine.id === 'desktop.local')) {
|
|
242
256
|
return riparaDesktopEngine(defsPath, defs, log);
|
|
243
257
|
}
|
|
244
258
|
const esito = aggiornaDefinizioni(defsPath, (dentro) => {
|
|
245
259
|
if (dentro.engines.some((engine) => engine.id === 'desktop.local')) return null;
|
|
246
|
-
if (dentro.engines.length >= CAPS.MAX_ENGINES)
|
|
260
|
+
if (dentro.engines.length >= CAPS.MAX_ENGINES) {
|
|
261
|
+
warnEngineCap(emit, 'desktop.local', dentro.engines.length);
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
247
264
|
const draft = draftFrom(dentro);
|
|
248
265
|
draft.engines.push(defaultDesktopEngine());
|
|
249
266
|
return draft;
|
|
250
|
-
}, { log });
|
|
267
|
+
}, { log: emit });
|
|
251
268
|
return esito || defs;
|
|
252
269
|
}
|
|
253
270
|
|
|
@@ -322,17 +339,21 @@ function eDaRiparare(defs) {
|
|
|
322
339
|
// invariato), rispetta il cap MAX_ENGINES. Non tocca CELLE.
|
|
323
340
|
function backfillKimiEngine(defsPath, defs, log) {
|
|
324
341
|
if (!defs) return defs;
|
|
342
|
+
const emit = typeof log === 'function' ? log : console.warn;
|
|
325
343
|
// Le condizioni si valutano su cio' che si legge DENTRO il lock, non
|
|
326
344
|
// sullo stato che avevamo in mano: e' la differenza fra decidere sul
|
|
327
345
|
// presente e decidere su una fotografia.
|
|
328
346
|
const esito = aggiornaDefinizioni(defsPath, (dentro) => {
|
|
329
347
|
if (dentro.engines.some((engine) => engine.managed?.client === 'kimi')) return null;
|
|
330
348
|
if (dentro.engines.some((engine) => engine.id === 'kimi.native')) return null;
|
|
331
|
-
if (dentro.engines.length >= CAPS.MAX_ENGINES)
|
|
349
|
+
if (dentro.engines.length >= CAPS.MAX_ENGINES) {
|
|
350
|
+
warnEngineCap(emit, 'kimi.native', dentro.engines.length);
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
332
353
|
const draft = draftFrom(dentro);
|
|
333
354
|
draft.engines.push(defaultKimiEngine());
|
|
334
355
|
return draft;
|
|
335
|
-
}, { log });
|
|
356
|
+
}, { log: emit });
|
|
336
357
|
return esito || defs;
|
|
337
358
|
}
|
|
338
359
|
|
|
@@ -345,6 +366,7 @@ function backfillKimiEngine(defsPath, defs, log) {
|
|
|
345
366
|
// MAX_ENGINES. Non tocca CELLE.
|
|
346
367
|
function backfillGrokEngine(defsPath, defs, cfg = {}) {
|
|
347
368
|
if (!defs) return defs;
|
|
369
|
+
const emit = typeof cfg.log === 'function' ? cfg.log : console.warn;
|
|
348
370
|
const platform = cfg.platform || process.platform;
|
|
349
371
|
const termux = platform === 'android'
|
|
350
372
|
|| termuxRuntimePaths(cfg.env || process.env, { platform, home: cfg.home }) !== null;
|
|
@@ -354,11 +376,14 @@ function backfillGrokEngine(defsPath, defs, cfg = {}) {
|
|
|
354
376
|
const esito = aggiornaDefinizioni(defsPath, (dentro) => {
|
|
355
377
|
if (dentro.engines.some((engine) => engine.managed?.client === 'grok')) return null;
|
|
356
378
|
if (dentro.engines.some((engine) => engine.id === 'grok.native')) return null;
|
|
357
|
-
if (dentro.engines.length >= CAPS.MAX_ENGINES)
|
|
379
|
+
if (dentro.engines.length >= CAPS.MAX_ENGINES) {
|
|
380
|
+
warnEngineCap(emit, 'grok.native', dentro.engines.length);
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
358
383
|
const draft = draftFrom(dentro);
|
|
359
384
|
draft.engines.push(defaultGrokEngine());
|
|
360
385
|
return draft;
|
|
361
|
-
}, { log:
|
|
386
|
+
}, { log: emit });
|
|
362
387
|
return esito || defs;
|
|
363
388
|
}
|
|
364
389
|
|
|
@@ -372,17 +397,21 @@ function backfillGrokEngine(defsPath, defs, cfg = {}) {
|
|
|
372
397
|
// MAX_ENGINES. Non tocca CELLE.
|
|
373
398
|
function backfillVlEngine(defsPath, defs, log) {
|
|
374
399
|
if (!defs) return defs;
|
|
400
|
+
const emit = typeof log === 'function' ? log : console.warn;
|
|
375
401
|
// Le condizioni si valutano su cio' che si legge DENTRO il lock, non
|
|
376
402
|
// sullo stato che avevamo in mano: e' la differenza fra decidere sul
|
|
377
403
|
// presente e decidere su una fotografia.
|
|
378
404
|
const esito = aggiornaDefinizioni(defsPath, (dentro) => {
|
|
379
405
|
if (dentro.engines.some((engine) => engine.managed?.client === 'vl')) return null;
|
|
380
406
|
if (dentro.engines.some((engine) => engine.id === 'vl.native')) return null;
|
|
381
|
-
if (dentro.engines.length >= CAPS.MAX_ENGINES)
|
|
407
|
+
if (dentro.engines.length >= CAPS.MAX_ENGINES) {
|
|
408
|
+
warnEngineCap(emit, 'vl.native', dentro.engines.length);
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
382
411
|
const draft = draftFrom(dentro);
|
|
383
412
|
draft.engines.push(defaultVlEngine());
|
|
384
413
|
return draft;
|
|
385
|
-
}, { log });
|
|
414
|
+
}, { log: emit });
|
|
386
415
|
return esito || defs;
|
|
387
416
|
}
|
|
388
417
|
|
|
@@ -576,9 +605,16 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
576
605
|
// fallito, non che ci sia stata un'eccezione.
|
|
577
606
|
const bootStat = {};
|
|
578
607
|
let boot = loadDefinitions(defsPath, bootStat);
|
|
579
|
-
if (!boot)
|
|
580
|
-
|
|
581
|
-
|
|
608
|
+
if (!boot) {
|
|
609
|
+
if (bootStat.parseReason) {
|
|
610
|
+
const emit = typeof cfg.log === 'function' ? cfg.log : console.warn;
|
|
611
|
+
emit(`WARN fleet definitions: ${bootStat.parseReason}`);
|
|
612
|
+
return { ...off, reason: bootStat.parseReason };
|
|
613
|
+
}
|
|
614
|
+
return bootStat.lstatBlocked
|
|
615
|
+
? { ...off, reason: `fleet.json non verificabile (${bootStat.lstatBlocked}): fail-closed, non "assente" né "invalido"` }
|
|
616
|
+
: off;
|
|
617
|
+
}
|
|
582
618
|
|
|
583
619
|
if (!readonly()) {
|
|
584
620
|
// Audit 093, rilievo 3: lo snapshot della BASE si prende QUI, alla lettura
|
|
@@ -1014,8 +1050,8 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
1014
1050
|
throw httpError(400, 'modello senza id o engine');
|
|
1015
1051
|
}
|
|
1016
1052
|
}
|
|
1017
|
-
if (!Array.isArray(engines) || engines.length < 1 || engines.length >
|
|
1018
|
-
throw httpError(400,
|
|
1053
|
+
if (!Array.isArray(engines) || engines.length < 1 || engines.length > CAPS.MAX_ENGINES) {
|
|
1054
|
+
throw httpError(400, `engines deve contenere 1..${CAPS.MAX_ENGINES} definizioni`);
|
|
1019
1055
|
}
|
|
1020
1056
|
const allowed = new Set(['id', 'label', 'rc', 'managed', 'command', 'args', 'envKeys', 'model', 'promptMode', 'promptFlag']);
|
|
1021
1057
|
const seen = new Set();
|
|
@@ -140,9 +140,9 @@
|
|
|
140
140
|
"supports_search_tool": false
|
|
141
141
|
},
|
|
142
142
|
{
|
|
143
|
-
"slug": "grok-4.
|
|
144
|
-
"display_name": "grok-4.
|
|
145
|
-
"description": "OpenCode Go: grok-4.
|
|
143
|
+
"slug": "grok-4.6",
|
|
144
|
+
"display_name": "grok-4.6",
|
|
145
|
+
"description": "OpenCode Go: grok-4.6",
|
|
146
146
|
"default_reasoning_level": "medium",
|
|
147
147
|
"supported_reasoning_levels": [
|
|
148
148
|
{
|
package/lib/fleet/definitions.js
CHANGED
|
@@ -13,9 +13,10 @@ const path = require('node:path');
|
|
|
13
13
|
const crypto = require('node:crypto');
|
|
14
14
|
const { normalizeManagedSpec } = require('./managed.js');
|
|
15
15
|
|
|
16
|
-
// --- Cap + identita' (dichiarati;
|
|
16
|
+
// --- Cap + identita' (dichiarati; 100 regge una flotta reale con margine;
|
|
17
|
+
// il limite resta deliberato contro un file patologico) ---
|
|
17
18
|
const SCHEMA_VERSION = 1;
|
|
18
|
-
const MAX_ENGINES =
|
|
19
|
+
const MAX_ENGINES = 100;
|
|
19
20
|
const MAX_CELLS = 32;
|
|
20
21
|
const MAX_ARGS = 32; // argv: array, mai stringa spezzata (no shell)
|
|
21
22
|
const MAX_ARG_LEN = 1024; // 1 KB per arg
|
|
@@ -204,7 +205,11 @@ function parseModel(m) {
|
|
|
204
205
|
// parseDefinitions(raw) -> {schemaVersion, engines, cells, models} | null
|
|
205
206
|
// Accetta stringa JSON o oggetto gia' parsato. Strict + fail-closed.
|
|
206
207
|
// ---------------------------------------------------------------------------
|
|
207
|
-
function parseDefinitions(raw, { allowLegacyTmuxNames = true } = {}) {
|
|
208
|
+
function parseDefinitions(raw, { allowLegacyTmuxNames = true, onReject = null } = {}) {
|
|
209
|
+
const reject = (message) => {
|
|
210
|
+
if (typeof onReject === 'function') onReject(message);
|
|
211
|
+
return null;
|
|
212
|
+
};
|
|
208
213
|
try {
|
|
209
214
|
let d;
|
|
210
215
|
if (typeof raw === 'string') {
|
|
@@ -217,7 +222,9 @@ function parseDefinitions(raw, { allowLegacyTmuxNames = true } = {}) {
|
|
|
217
222
|
|
|
218
223
|
if (d.schemaVersion !== SCHEMA_VERSION) return null;
|
|
219
224
|
if (!Array.isArray(d.engines)) return null; // engines obbligatorio (array)
|
|
220
|
-
if (d.engines.length > MAX_ENGINES)
|
|
225
|
+
if (d.engines.length > MAX_ENGINES) {
|
|
226
|
+
return reject(`fleet.json rifiutato: ${d.engines.length} engine dichiarati, cap ${MAX_ENGINES}; riduci gli engine a ${MAX_ENGINES} o meno`);
|
|
227
|
+
}
|
|
221
228
|
if (!Array.isArray(d.cells)) return null; // cells obbligatorio (array)
|
|
222
229
|
if (d.cells.length > MAX_CELLS) return null;
|
|
223
230
|
|
|
@@ -269,7 +276,7 @@ function parseDefinitions(raw, { allowLegacyTmuxNames = true } = {}) {
|
|
|
269
276
|
const cellIds = new Set();
|
|
270
277
|
const cells = [];
|
|
271
278
|
for (const c of d.cells) {
|
|
272
|
-
const cell = parseCell(c, engineIds, engineMap, { allowLegacyTmuxNames, extraModels });
|
|
279
|
+
const cell = parseCell(c, engineIds, engineMap, { allowLegacyTmuxNames, extraModels, onReject: reject });
|
|
273
280
|
if (!cell) return null;
|
|
274
281
|
if (cellIds.has(cell.id)) return null; // id cell univoco
|
|
275
282
|
cellIds.add(cell.id);
|
|
@@ -430,7 +437,11 @@ function cellModelAllowed(engineId, model, engineMap, extraModels) {
|
|
|
430
437
|
return !!normalizeManagedSpec({ ...engine.managed, model }, { extraModels });
|
|
431
438
|
}
|
|
432
439
|
|
|
433
|
-
function parseCell(c, engineIds, engineMap = new Map(), { allowLegacyTmuxNames = true, extraModels = null } = {}) {
|
|
440
|
+
function parseCell(c, engineIds, engineMap = new Map(), { allowLegacyTmuxNames = true, extraModels = null, onReject = null } = {}) {
|
|
441
|
+
const reject = (message) => {
|
|
442
|
+
if (typeof onReject === 'function') onReject(message);
|
|
443
|
+
return null;
|
|
444
|
+
};
|
|
434
445
|
if (!c || typeof c !== 'object' || Array.isArray(c)) return null;
|
|
435
446
|
|
|
436
447
|
// id
|
|
@@ -475,7 +486,9 @@ function parseCell(c, engineIds, engineMap = new Map(), { allowLegacyTmuxNames =
|
|
|
475
486
|
if (c.models !== undefined) {
|
|
476
487
|
if (!c.models || typeof c.models !== 'object' || Array.isArray(c.models)) return null;
|
|
477
488
|
const entries = Object.entries(c.models);
|
|
478
|
-
if (entries.length > MAX_ENGINES)
|
|
489
|
+
if (entries.length > MAX_ENGINES) {
|
|
490
|
+
return reject(`fleet.json rifiutato: la cella ${c.id} contiene ${entries.length} modelli ricordati, cap ${MAX_ENGINES}; riduci la mappa a ${MAX_ENGINES} o meno`);
|
|
491
|
+
}
|
|
479
492
|
for (const [engineId, value] of entries) {
|
|
480
493
|
if (!engineIds.has(engineId) || typeof value !== 'string' || !value || value.length > MAX_MODEL_VAL_LEN) return null;
|
|
481
494
|
// Anche la memoria per-engine passa dal gate: e' un valore che tornera'
|
|
@@ -494,7 +507,9 @@ function parseCell(c, engineIds, engineMap = new Map(), { allowLegacyTmuxNames =
|
|
|
494
507
|
if (c.permissionPolicies !== undefined) {
|
|
495
508
|
if (!c.permissionPolicies || typeof c.permissionPolicies !== 'object' || Array.isArray(c.permissionPolicies)) return null;
|
|
496
509
|
const entries = Object.entries(c.permissionPolicies);
|
|
497
|
-
if (entries.length > MAX_ENGINES)
|
|
510
|
+
if (entries.length > MAX_ENGINES) {
|
|
511
|
+
return reject(`fleet.json rifiutato: la cella ${c.id} contiene ${entries.length} permissionPolicies, cap ${MAX_ENGINES}; riduci la mappa a ${MAX_ENGINES} o meno`);
|
|
512
|
+
}
|
|
498
513
|
permissionPolicies = {};
|
|
499
514
|
for (const [engineId, value] of entries) {
|
|
500
515
|
if (!engineIds.has(engineId)) return null;
|
|
@@ -511,7 +526,9 @@ function parseCell(c, engineIds, engineMap = new Map(), { allowLegacyTmuxNames =
|
|
|
511
526
|
if (c.commands !== undefined) {
|
|
512
527
|
if (!c.commands || typeof c.commands !== 'object' || Array.isArray(c.commands)) return null;
|
|
513
528
|
const entries = Object.entries(c.commands);
|
|
514
|
-
if (entries.length > MAX_ENGINES)
|
|
529
|
+
if (entries.length > MAX_ENGINES) {
|
|
530
|
+
return reject(`fleet.json rifiutato: la cella ${c.id} contiene ${entries.length} commands, cap ${MAX_ENGINES}; riduci la mappa a ${MAX_ENGINES} o meno`);
|
|
531
|
+
}
|
|
515
532
|
commands = {};
|
|
516
533
|
for (const [engineId, value] of entries) {
|
|
517
534
|
if (!engineIds.has(engineId) || engineMap.get(engineId)?.managed?.client !== 'shell') return null;
|
|
@@ -980,7 +997,9 @@ function loadDefinitions(p, out) {
|
|
|
980
997
|
if (st.isSymbolicLink()) return null; // no symlink
|
|
981
998
|
if (!st.isFile()) return null;
|
|
982
999
|
const raw = fs.readFileSync(p, 'utf8');
|
|
983
|
-
return parseDefinitions(raw
|
|
1000
|
+
return parseDefinitions(raw, {
|
|
1001
|
+
onReject: out ? (reason) => { out.parseReason = reason; } : null,
|
|
1002
|
+
});
|
|
984
1003
|
} catch (_) { return null; }
|
|
985
1004
|
}
|
|
986
1005
|
|
package/lib/fleet/managed.js
CHANGED
|
@@ -13,12 +13,57 @@ const { readCredentialStore, safePrivateDir } = require('./credentials.js');
|
|
|
13
13
|
const OLLAMA_CLOUD_MODELS = Object.freeze([
|
|
14
14
|
'glm-5.2', 'kimi-k2.7-code', 'deepseek-v4-pro', 'minimax-m3',
|
|
15
15
|
'qwen3.5:397b', 'deepseek-v4-flash', 'mistral-large-3:675b', 'gemma4:31b',
|
|
16
|
+
// glm-5.3-flash: disponibile su ollama.com/library (2026-08-27; glm-5.3
|
|
17
|
+
// non-flash invece 404 su Ollama e NON va elencata qui). Gia` usata da
|
|
18
|
+
// celle esistenti su questo engine: senza voce in OLLAMA_CONTEXT il launch
|
|
19
|
+
// ricadeva sul fallback 200000 in silenzio.
|
|
20
|
+
'glm-5.3-flash',
|
|
16
21
|
]);
|
|
22
|
+
// Autorita' per OLLAMA_CONTEXT = IL CAMPO STRUTTURATO «Context» della scheda
|
|
23
|
+
// canale (ollama.com/library, fetch 2026-08-27) — NON la descrizione, che
|
|
24
|
+
// racconta il modello in astratto (su minimax-m3 dice 1M mentre il campo dice
|
|
25
|
+
// 512K). models.dev vale dove la riga provider esiste ed e' aggiornata.
|
|
26
|
+
// Criterio suffissi: numero esatto disponibile vince (glm-5.2 976K -> 976000;
|
|
27
|
+
// glm-5.3-flash 1M corroborato 1000000 da 7 provider su models.dev);
|
|
28
|
+
// altrimenti convenzione binaria del repo (deepseek 1M -> 1048576;
|
|
29
|
+
// minimax 512K -> 524288, valore pre-esistente ripristinato).
|
|
17
30
|
const OLLAMA_CONTEXT = Object.freeze({
|
|
18
|
-
|
|
19
|
-
'
|
|
31
|
+
// glm-5.2: scheda «976K context» (era 1M: over-declare di 24k).
|
|
32
|
+
'glm-5.2': 976000, 'kimi-k2.7-code': 262144,
|
|
33
|
+
// deepseek-v4-pro: scheda «1M context» (era 524288: META' del reale).
|
|
34
|
+
'deepseek-v4-pro': 1048576,
|
|
35
|
+
'minimax-m3': 524288,
|
|
36
|
+
'qwen3.5:397b': 262144, 'deepseek-v4-flash': 1048576,
|
|
20
37
|
'mistral-large-3:675b': 262144, 'gemma4:31b': 262144,
|
|
38
|
+
// glm-5.3-flash: scheda «1M context» + concorde zhipuai/zai/opencode-go.
|
|
39
|
+
'glm-5.3-flash': 1000000,
|
|
21
40
|
});
|
|
41
|
+
// Capacita' per modello OSSERVATE dalla scheda canale (ollama.com/library,
|
|
42
|
+
// 2026-08-27). Assenza = default conservativo (comportamento odierno, nessuna
|
|
43
|
+
// regressione). glm-5.3-flash e' nativamente multimodale con tools e thinking:
|
|
44
|
+
// senza queste voci il catalogo generato spegne vision e parallel tools anche
|
|
45
|
+
// a finestra corretta (il gating Image e' su model_info.input_modalities).
|
|
46
|
+
const OLLAMA_MODEL_CAPABILITIES = Object.freeze({
|
|
47
|
+
// vision/thinking/tools: dichiarati dalla scheda. PARALLEL NON dichiarato:
|
|
48
|
+
// default conservativo false finche' non misurato su device (i tool restano
|
|
49
|
+
// attivi; resta spento solo l'invocarli in parallelo). Accendere dopo smoke.
|
|
50
|
+
'glm-5.3-flash': Object.freeze({ input: ['text', 'image'], reasoning: true, supportsParallelToolCalls: false }),
|
|
51
|
+
});
|
|
52
|
+
// Descrittori dell'engine ollama-cloud per la generazione del catalogo client:
|
|
53
|
+
// stessa forma dei descrittori custom/PI (id, label, contextWindow, input,
|
|
54
|
+
// reasoning, supportsParallelToolCalls).
|
|
55
|
+
function ollamaCloudCatalogModels() {
|
|
56
|
+
return OLLAMA_CLOUD_MODELS.map((id) => {
|
|
57
|
+
const cap = OLLAMA_MODEL_CAPABILITIES[id] || {};
|
|
58
|
+
return Object.freeze({
|
|
59
|
+
id, label: id,
|
|
60
|
+
contextWindow: ollamaContextFor(id),
|
|
61
|
+
input: Object.freeze([...(cap.input || ['text'])]),
|
|
62
|
+
reasoning: cap.reasoning === true,
|
|
63
|
+
supportsParallelToolCalls: cap.supportsParallelToolCalls === true,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
22
67
|
// Lo stesso modello si scrive con o senza tag ('deepseek-v4-flash' e
|
|
23
68
|
// 'deepseek-v4-flash:0731'). Si prova prima la chiave esatta, perche' nella
|
|
24
69
|
// mappa ci sono nomi in cui il tag fa parte dell'identita' del modello
|
|
@@ -151,7 +196,9 @@ const ALIBABA_PI_MODELS = Object.freeze([
|
|
|
151
196
|
// xiaomi/mimo-v2.5*"). hy3-preview: "Model is unavailable".
|
|
152
197
|
// Il catalogo live li pubblicizza comunque; qui non entrano.
|
|
153
198
|
// - grok-4.5 solo su Responses: su Chat risponde 503 e Messages lo rifiuta
|
|
154
|
-
// esplicitamente ("not supported for format anthropic").
|
|
199
|
+
// esplicitamente ("not supported for format anthropic"). Dal 2026-08-27
|
|
200
|
+
// l'id in elenco e` grok-4.6 (stessi limiti misurati su models.dev); il
|
|
201
|
+
// commento sopra conserva la misura storica fatta su 4.5.
|
|
155
202
|
// - deepseek-v4-flash-vision-exp: MISURATO 2026-08-24 sul gateway
|
|
156
203
|
// opencode.ai/zen/go -> 200 su tutti e tre i wire (Responses
|
|
157
204
|
// status=completed, Messages stop_reason=end_turn, Chat finish_reason=stop).
|
|
@@ -168,7 +215,7 @@ const OPENCODE_GO_MESSAGES_MODELS = Object.freeze([
|
|
|
168
215
|
'qwen3.8-max', 'qwen3.7-max', 'qwen3.7-plus', 'qwen3.6-plus', 'qwen3.5-plus',
|
|
169
216
|
]);
|
|
170
217
|
const OPENCODE_GO_RESPONSES_MODELS = Object.freeze([
|
|
171
|
-
'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp', 'deepseek-v4-pro', 'gpt-5.6-luna', 'grok-4.
|
|
218
|
+
'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp', 'deepseek-v4-pro', 'gpt-5.6-luna', 'grok-4.6', 'glm-5.2', 'glm-5.1', 'glm-5',
|
|
172
219
|
]);
|
|
173
220
|
const OPENCODE_GO_CHAT_MODELS = Object.freeze([
|
|
174
221
|
'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp', 'deepseek-v4-pro', 'glm-5.2', 'glm-5.1', 'glm-5',
|
|
@@ -212,7 +259,9 @@ const OPENCODE_GO_LIMITS = Object.freeze({
|
|
|
212
259
|
'mimo-v2.5-pro': { context: 1048576, output: 128000 },
|
|
213
260
|
hy3: { context: 256000, output: 64000 },
|
|
214
261
|
'gpt-5.6-luna': { context: 1050000, output: 128000 },
|
|
215
|
-
|
|
262
|
+
// grok-4.5 -> grok-4.6 (2026-08-27): id aggiornato al modello reale;
|
|
263
|
+
// limiti confermati da models.dev (xai e opencode-go: 500000/500000).
|
|
264
|
+
'grok-4.6': { context: 500000, output: 500000 },
|
|
216
265
|
});
|
|
217
266
|
|
|
218
267
|
function opencodeGoContextFor(model) {
|
|
@@ -296,7 +345,7 @@ const CATALOG = Object.freeze([
|
|
|
296
345
|
// deve dire la verita' su cosa stiamo usando. Il suffisso `[1m]` resta: e' un
|
|
297
346
|
// flag di finestra del CLI e viene tolto prima della richiesta HTTP, dove
|
|
298
347
|
// `glm-5.3[1m]` letterale darebbe 400 (code 1214, misurato).
|
|
299
|
-
{ 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 },
|
|
348
|
+
{ 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]', 'glm-5.3-flash'], core: true },
|
|
300
349
|
// OpenCode Go su Claude parla Anthropic Messages, e la wire accetta SOLO
|
|
301
350
|
// `x-api-key`: con `Authorization: Bearer` risponde 401 AuthError. Per questo
|
|
302
351
|
// l'endpoint e' la root senza `/v1` (il client aggiunge `/v1/messages`) e il
|
|
@@ -433,8 +482,8 @@ const CATALOG = Object.freeze([
|
|
|
433
482
|
// --- Sezione legacy --------------------------------------------------------
|
|
434
483
|
// Compatibilita' sola lettura/launch per configurazioni 0.8.0: mai nel catalogo
|
|
435
484
|
// UI (publicCatalog filtra `legacy`). Risolti solo da profileFor/normalizeManagedSpec.
|
|
436
|
-
{ 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 },
|
|
437
|
-
{ 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 },
|
|
485
|
+
{ 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]', 'glm-5.3-flash'], legacySecrets: true, legacyProvider: 'zai-a', legacy: true },
|
|
486
|
+
{ 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]', 'glm-5.3-flash'], legacySecrets: true, legacyProvider: 'zai-p', legacy: true },
|
|
438
487
|
]);
|
|
439
488
|
|
|
440
489
|
function profileFor(client, provider, credentialProfile) {
|
|
@@ -1510,6 +1559,8 @@ function customCatalogFor(spec, model, declaredModels, home) {
|
|
|
1510
1559
|
: [{ effort: 'low', description: 'Fast responses with lighter reasoning' },
|
|
1511
1560
|
{ effort: 'medium', description: 'Balanced reasoning depth' },
|
|
1512
1561
|
{ effort: 'high', description: 'Greater reasoning depth for complex problems' }],
|
|
1562
|
+
input_modalities: m.input || ['text'],
|
|
1563
|
+
supports_parallel_tool_calls: m.supportsParallelToolCalls === true,
|
|
1513
1564
|
shell_type: 'default',
|
|
1514
1565
|
visibility: 'list',
|
|
1515
1566
|
supported_in_api: true,
|
|
@@ -1524,12 +1575,10 @@ function customCatalogFor(spec, model, declaredModels, home) {
|
|
|
1524
1575
|
apply_patch_tool_type: null,
|
|
1525
1576
|
web_search_tool_type: 'text',
|
|
1526
1577
|
truncation_policy: { mode: 'tokens', limit: m.maxTokens || m.contextWindow || 128000 },
|
|
1527
|
-
supports_parallel_tool_calls: false,
|
|
1528
1578
|
supports_image_detail_original: false,
|
|
1529
1579
|
context_window: m.contextWindow || 128000,
|
|
1530
1580
|
effective_context_window_percent: 95,
|
|
1531
1581
|
experimental_supported_tools: [],
|
|
1532
|
-
input_modalities: ['text'],
|
|
1533
1582
|
supports_search_tool: false,
|
|
1534
1583
|
};
|
|
1535
1584
|
}),
|
|
@@ -1543,8 +1592,12 @@ function customCatalogFor(spec, model, declaredModels, home) {
|
|
|
1543
1592
|
else throw e;
|
|
1544
1593
|
}
|
|
1545
1594
|
fs.chmodSync(dir, 0o700);
|
|
1546
|
-
|
|
1547
|
-
|
|
1595
|
+
// Nome generico per engine gestito: i custom hanno providerId proprio, gli
|
|
1596
|
+
// altri usano client.provider cosi' QUALSIASI engine che dichiara modelli
|
|
1597
|
+
// ottiene il suo catalogo senza casi speciali per provider.
|
|
1598
|
+
const catalogId = spec.providerId || `${spec.client}.${spec.provider}`;
|
|
1599
|
+
const target = path.join(dir, `${catalogId}.json`);
|
|
1600
|
+
const tmp = path.join(dir, `.${catalogId}.${crypto.randomBytes(6).toString('hex')}.tmp`);
|
|
1548
1601
|
try {
|
|
1549
1602
|
fs.writeFileSync(tmp, JSON.stringify(cat), { mode: 0o600 });
|
|
1550
1603
|
fs.chmodSync(tmp, 0o600);
|
|
@@ -1734,8 +1787,26 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
1734
1787
|
env.OPENAI_API_KEY = cred.value;
|
|
1735
1788
|
args.push(...codexProviderArgs('ollama_cloud', 'Ollama Cloud', profile.endpoint, 'OPENAI_API_KEY'));
|
|
1736
1789
|
args.push('-c', 'model_providers.ollama_cloud.stream_idle_timeout_ms=600000', '-c', `model_context_window=${ollamaContextFor(model) ?? 200000}`);
|
|
1737
|
-
|
|
1738
|
-
|
|
1790
|
+
// Catalogo generato dagli id DICHIARATI DELL'ENGINE: senza entry il
|
|
1791
|
+
// client cade sul descrittore fallback (272000 fisso, parallel tools
|
|
1792
|
+
// false, nessun reasoning, nessuna modalita' visiva — guasto misurato
|
|
1793
|
+
// su glm-5.3-flash, controllo 2026-08-27). Il file
|
|
1794
|
+
// utente resta come fallback se la generazione non e' possibile.
|
|
1795
|
+
let catalogMeta = null;
|
|
1796
|
+
try {
|
|
1797
|
+
catalogMeta = customCatalogFor(spec, model, ollamaCloudCatalogModels(), home);
|
|
1798
|
+
} catch (_) {
|
|
1799
|
+
// Generazione impossibile (es. path occupato da una directory):
|
|
1800
|
+
// NON deve far fallire il launch — si ricade sul file utente sotto.
|
|
1801
|
+
catalogMeta = null;
|
|
1802
|
+
}
|
|
1803
|
+
if (catalogMeta) {
|
|
1804
|
+
args.push('-c', `model_catalog_json=${JSON.stringify(catalogMeta.catalogPath)}`);
|
|
1805
|
+
if (catalogMeta.contextWindow) args.push('-c', `model_context_window=${catalogMeta.contextWindow}`);
|
|
1806
|
+
} else {
|
|
1807
|
+
const localCatalog = path.join(home, '.codex', 'ollama_cloud_model_catalog.json');
|
|
1808
|
+
if (fs.existsSync(localCatalog)) args.push('-c', `model_catalog_json="${localCatalog}"`);
|
|
1809
|
+
}
|
|
1739
1810
|
} else if (spec.provider === 'openrouter') {
|
|
1740
1811
|
env.OPENROUTER_API_KEY = cred.value;
|
|
1741
1812
|
const authHelper = path.join(__dirname, 'openrouter-auth-helper.js');
|
|
@@ -1950,7 +2021,7 @@ function publicCatalog() {
|
|
|
1950
2021
|
|
|
1951
2022
|
module.exports = {
|
|
1952
2023
|
knownMcpServerNames,
|
|
1953
|
-
CATALOG, OLLAMA_CLOUD_MODELS, OLLAMA_CONTEXT, ALIBABA_TOKEN_PLAN_MODELS,
|
|
2024
|
+
CATALOG, OLLAMA_CLOUD_MODELS, OLLAMA_CONTEXT, OLLAMA_MODEL_CAPABILITIES, ollamaCloudCatalogModels, ALIBABA_TOKEN_PLAN_MODELS,
|
|
1954
2025
|
ALIBABA_CODEX_MODELS, ALIBABA_TOKEN_PLAN_CONTEXT, ALIBABA_PI_MODELS,
|
|
1955
2026
|
OPENCODE_GO_MESSAGES_MODELS, OPENCODE_GO_RESPONSES_MODELS, OPENCODE_GO_CHAT_MODELS, OPENCODE_GO_LIMITS,
|
|
1956
2027
|
OPENCODE_GO_ANTHROPIC_ROOT, OPENCODE_GO_API_BASE,
|