@mmmbuto/nexuscrew 0.8.30 → 0.8.32
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 +68 -0
- package/README.md +26 -8
- package/frontend/dist/assets/index-CrggFFKZ.css +32 -0
- package/frontend/dist/assets/index-Dw90VJRY.js +93 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/fleet/builtin.js +65 -12
- package/lib/fleet/cell-hold.js +20 -0
- package/lib/fleet/definitions.js +113 -15
- package/lib/fleet/launch-broker.js +4 -1
- package/lib/fleet/launch.js +124 -0
- package/lib/fleet/managed.js +49 -7
- package/lib/fleet/provider.js +1 -1
- package/lib/fleet/runtime.js +90 -14
- package/lib/mcp/tools.js +2 -1
- package/lib/proxy/federation.js +63 -2
- package/lib/server.js +22 -6
- package/lib/settings/routes.js +60 -14
- package/lib/tmux/lifecycle.js +1 -0
- package/package.json +1 -1
- package/frontend/dist/assets/index-CJtVoawR.css +0 -32
- package/frontend/dist/assets/index-CPNWWnqN.js +0 -93
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-Dw90VJRY.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CrggFFKZ.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="root"></div>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.8.
|
|
1
|
+
{"version":"0.8.32"}
|
package/lib/fleet/builtin.js
CHANGED
|
@@ -30,15 +30,16 @@ const os = require('node:os');
|
|
|
30
30
|
const path = require('node:path');
|
|
31
31
|
const {
|
|
32
32
|
loadDefinitions, atomicWrite, CAPS, MAX_CELLS, validTmuxName,
|
|
33
|
+
cellIdFromTmuxSession,
|
|
33
34
|
resolveCwd, normalizeCwdRel, deriveCwdRel,
|
|
34
35
|
} = require('./definitions.js');
|
|
35
36
|
const {
|
|
36
|
-
publicCatalog, describeManaged, describeCatalogCredential, defaultShellEngine,
|
|
37
|
+
publicCatalog, describeManaged, describeCatalogCredential, defaultShellEngine, defaultAgyEngine,
|
|
37
38
|
} = require('./managed.js');
|
|
38
39
|
const { validEnvKey } = require('./env-key.js');
|
|
39
40
|
const { setCredential, removeCredential } = require('./credentials.js');
|
|
40
41
|
const { createLaunchBroker } = require('./launch-broker.js');
|
|
41
|
-
const { MINIMAL_ENV_KEYS } = require('../runtime/env.js');
|
|
42
|
+
const { MINIMAL_ENV_KEYS, termuxRuntimePaths } = require('../runtime/env.js');
|
|
42
43
|
const { requireSharedTmuxProtection } = require('../tmux/shared-server.js');
|
|
43
44
|
|
|
44
45
|
// Toolkit stateless + runtime estratti (behavior-preserving). I simboli di
|
|
@@ -47,7 +48,7 @@ const { requireSharedTmuxProtection } = require('../tmux/shared-server.js');
|
|
|
47
48
|
const { createBuiltinRuntime } = require('./runtime.js');
|
|
48
49
|
const {
|
|
49
50
|
composeLaunchArgv, composeClientInvocation, minimalEnv, promptCharsOk,
|
|
50
|
-
redactSecrets, sanitizeEarlyDiagnostic, waitStablePane, httpError,
|
|
51
|
+
redactSecrets, sanitizeEarlyDiagnostic, waitStablePane, httpError, migrateLegacyTmuxSessions,
|
|
51
52
|
} = require('./launch.js');
|
|
52
53
|
|
|
53
54
|
// Copia deep delle definizioni per il draft di mutazione (round-trip sicuro su disco).
|
|
@@ -83,6 +84,29 @@ function backfillShellEngine(defsPath, defs) {
|
|
|
83
84
|
try { return atomicWrite(defsPath, draft); } catch (_) { return defs; }
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
// Backfill platform-aware dell'engine Agy primario (design §4.2): installazioni
|
|
88
|
+
// esistenti su Linux/macOS non-Termux ricevono agy.native in modo idempotente e
|
|
89
|
+
// non distruttivo. Su altre piattaforme (Termux/Windows) e' un no-op: agy non e'
|
|
90
|
+
// un client primario supportato li' e l'utente puo comunque usarlo via shell.local
|
|
91
|
+
// con command per-cella. Idempotente (gia' presente -> skip), NON sovrascrive un
|
|
92
|
+
// id 'agy.native' gia' scelto dall'utente per altro (collisione -> skip, store
|
|
93
|
+
// invariato), rispetta il cap MAX_ENGINES. Non tocca CELLE: una cella esistente
|
|
94
|
+
// con id agy.native non cambia engine silenziosamente (il passaggio a agy.native
|
|
95
|
+
// avviene solo nel field gate esplicito del coordinator).
|
|
96
|
+
function backfillAgyEngine(defsPath, defs, cfg = {}) {
|
|
97
|
+
if (!defs) return defs;
|
|
98
|
+
const platform = cfg.platform || process.platform;
|
|
99
|
+
const termux = platform === 'android'
|
|
100
|
+
|| termuxRuntimePaths(cfg.env || process.env, { platform, home: cfg.home }) !== null;
|
|
101
|
+
if (termux || (platform !== 'linux' && platform !== 'darwin')) return defs;
|
|
102
|
+
if (defs.engines.some((engine) => engine.managed?.client === 'agy')) return defs;
|
|
103
|
+
if (defs.engines.some((engine) => engine.id === 'agy.native')) return defs;
|
|
104
|
+
if (defs.engines.length >= CAPS.MAX_ENGINES) return defs;
|
|
105
|
+
const draft = draftFrom(defs);
|
|
106
|
+
draft.engines.push(defaultAgyEngine());
|
|
107
|
+
try { return atomicWrite(defsPath, draft); } catch (_) { return defs; }
|
|
108
|
+
}
|
|
109
|
+
|
|
86
110
|
// Applica engine + modello + policy come un'unica transizione. Ogni engine ricorda
|
|
87
111
|
// il proprio ultimo modello E l'ultima policy; passando a un altro engine né
|
|
88
112
|
// l'uno né l'altra attraversano il confine. La policy e' PER-CELL PER-ENGINE:
|
|
@@ -215,6 +239,7 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
215
239
|
available: false, provider: 'builtin',
|
|
216
240
|
isCellSession: () => false, capabilities: () => [],
|
|
217
241
|
};
|
|
242
|
+
const blocked = (reason, code) => ({ ...off, reason, ...(code ? { migrationCode: code } : {}) });
|
|
218
243
|
if (cfg.builtinEnabled === false) return off;
|
|
219
244
|
|
|
220
245
|
const home = cfg.home || os.homedir();
|
|
@@ -226,7 +251,31 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
226
251
|
// Bootstrap: fleet.json valido? garbage -> unavailable (fail-closed, design §7)
|
|
227
252
|
let boot = loadDefinitions(defsPath);
|
|
228
253
|
if (!boot) return off;
|
|
229
|
-
if (!readonly())
|
|
254
|
+
if (!readonly()) {
|
|
255
|
+
// La migrazione precede QUALUNQUE backfill/scrittura: loadDefinitions
|
|
256
|
+
// normalizza i nomi legacy soltanto in memoria. Se il rename e' ambiguo o
|
|
257
|
+
// fallisce, fleet.json resta byte-invariato e la Fleet non puo creare una
|
|
258
|
+
// seconda sessione safe accanto a quella legacy.
|
|
259
|
+
let migration;
|
|
260
|
+
try {
|
|
261
|
+
migration = await migrateLegacyTmuxSessions(tmuxBin, boot, { readonly: false });
|
|
262
|
+
} catch (error) {
|
|
263
|
+
const code = /^[A-Z0-9_]+$/.test(String(error?.code || ''))
|
|
264
|
+
? String(error.code) : 'TMUX_MIGRATION_FAILED';
|
|
265
|
+
const detail = String(error?.message || 'migrazione tmux fallita')
|
|
266
|
+
.replace(/[\x00-\x1f\x7f]+/g, ' ').trim().slice(0, 320);
|
|
267
|
+
return blocked(`${detail} [${code}]`, code);
|
|
268
|
+
}
|
|
269
|
+
if (migration.needsPersistence) {
|
|
270
|
+
try { boot = atomicWrite(defsPath, boot); }
|
|
271
|
+
catch (_) {
|
|
272
|
+
return blocked('migrazione tmux completata ma fleet.json non e persistibile [TMUX_MIGRATION_PERSIST_FAILED]',
|
|
273
|
+
'TMUX_MIGRATION_PERSIST_FAILED');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
boot = backfillShellEngine(defsPath, boot);
|
|
277
|
+
boot = backfillAgyEngine(defsPath, boot, cfg);
|
|
278
|
+
}
|
|
230
279
|
|
|
231
280
|
// Adopt or create the shared server before exposing a mutable Fleet. Reapply
|
|
232
281
|
// before every lifecycle mutation so an accidental config reload cannot
|
|
@@ -585,15 +634,18 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
585
634
|
const CELL_ID_RE = /^[A-Za-z0-9._-]{1,32}$/;
|
|
586
635
|
function deriveCellId(session, cells) {
|
|
587
636
|
const raw = String(session || '');
|
|
588
|
-
//
|
|
589
|
-
//
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
637
|
+
// Sessione canonica fleet (v2 per id puntati, o cloud-<id> senza punto):
|
|
638
|
+
// il reverse ricostruisce il cellId umano esatto (round-trip verificato).
|
|
639
|
+
const reversed = cellIdFromTmuxSession(raw);
|
|
640
|
+
if (reversed) {
|
|
641
|
+
const used = new Set(cells.map((c) => c.id));
|
|
642
|
+
if (!used.has(reversed)) return reversed;
|
|
643
|
+
return reversed; // id gia' in uso -> il caller produrra' un 409 esplicito
|
|
644
|
+
}
|
|
645
|
+
// fallback: nome non canonico (override custom, "jarvis", ...): sanitizza
|
|
646
|
+
let base = raw.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 32) || 'cell';
|
|
594
647
|
const used = new Set(cells.map((c) => c.id));
|
|
595
648
|
if (!used.has(base)) return base;
|
|
596
|
-
if (canonicalSuffix) return base; // il caller produrrà un 409 esplicito
|
|
597
649
|
for (let i = 2; i < 100; i += 1) {
|
|
598
650
|
const candidate = `${base.slice(0, Math.max(1, 32 - String(i).length - 1))}-${i}`;
|
|
599
651
|
if (!used.has(candidate)) return candidate;
|
|
@@ -756,7 +808,7 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
756
808
|
rc: { type: 'boolean', required: false, default: false },
|
|
757
809
|
managed: {
|
|
758
810
|
type: 'object', requiredFor: 'managed',
|
|
759
|
-
client: { type: 'enum', values: ['claude', 'codex', 'codex-vl', 'pi', 'shell'] },
|
|
811
|
+
client: { type: 'enum', values: ['claude', 'codex', 'codex-vl', 'pi', 'agy', 'shell'] },
|
|
760
812
|
provider: { type: 'catalog', source: 'managedCatalog' },
|
|
761
813
|
credentialProfile: { type: 'string', required: false, max: 32 },
|
|
762
814
|
model: { type: 'string', required: false, max: CAPS.MAX_MODEL_VAL_LEN },
|
|
@@ -820,6 +872,7 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
820
872
|
module.exports = {
|
|
821
873
|
createBuiltinFleet,
|
|
822
874
|
backfillShellEngine,
|
|
875
|
+
backfillAgyEngine,
|
|
823
876
|
resolveCellCwd,
|
|
824
877
|
composeLaunchArgv,
|
|
825
878
|
composeClientInvocation,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// lib/fleet/cell-hold.js — placeholder inerte per l'avvio staged del pane Fleet.
|
|
4
|
+
// (NexusCrew 0.8.31 — design piano §3.3: identita tmux sicura)
|
|
5
|
+
//
|
|
6
|
+
// Resta in attesa finche' `respawn-pane -k` non lo sostituisce con il vero client
|
|
7
|
+
// (cell-exec). Crea il pane e la finestra in modo deterministico cosi' il runtime
|
|
8
|
+
// puo' armare `remain-on-exit` window-local PRIMA di lanciare il child reale:
|
|
9
|
+
// cio' impedisce a un child rapido di chiudere la sessione durante il setup. La
|
|
10
|
+
// normalizzazione dei punti nei nomi tmux e gestita separatamente dal mapping v2.
|
|
11
|
+
//
|
|
12
|
+
// Vincoli (piano §3.3): NESSUNA shell interattiva, NESSUN rc/alias/plugin utente,
|
|
13
|
+
// NESSUN dato sensibile, NESSUN argomento interpretato. argv diretto
|
|
14
|
+
// (process.execPath + path assoluto di questo file), puramente bloccante. Non e'
|
|
15
|
+
// il comando reale della cella: command/env/prompt/payload broker non appaiono
|
|
16
|
+
// mai nell'argv tmux.
|
|
17
|
+
//
|
|
18
|
+
// Uscita silenziosa sul segnale di terminazione inviato da respawn-pane -k.
|
|
19
|
+
|
|
20
|
+
setInterval(() => {}, 60000);
|
package/lib/fleet/definitions.js
CHANGED
|
@@ -33,9 +33,10 @@ const MAX_CELL_COMMAND_LEN = 4096;
|
|
|
33
33
|
const MAX_TMUXSESSION_LEN = 64;
|
|
34
34
|
|
|
35
35
|
const ENGINE_ID_RE = /^[a-z0-9._-]{1,32}$/; // engine id: lowercase (design 4a/9f)
|
|
36
|
-
const CELL_ID_RE = /^[A-Za-z0-9._-]{1,32}$/; // cell id: ammette maiuscole
|
|
36
|
+
const CELL_ID_RE = /^[A-Za-z0-9._-]{1,32}$/; // cell id: ammette maiuscole (il punto e' un id umano valido)
|
|
37
37
|
const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/; // identificatore env POSIX-like
|
|
38
|
-
const TMUX_NAME_RE = /^[\w.-]{1,64}$/; //
|
|
38
|
+
const TMUX_NAME_RE = /^[\w.-]{1,64}$/; // parsing: ammette il punto (legacy puntato da migrare)
|
|
39
|
+
const TMUX_SAFE_NAME_RE = /^[\w-]{1,64}$/; // scrittura: nomi tmux-safe (NO punto: tmux lo normalizza in '_')
|
|
39
40
|
|
|
40
41
|
// Denylist dura di chiavi loader/runtime (design 9a): chi le imposta altera
|
|
41
42
|
// l'esecuzione controllata dal service -> rifiuta l'INTERO documento.
|
|
@@ -76,11 +77,64 @@ function validTmuxName(name) {
|
|
|
76
77
|
&& !name.startsWith('-');
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
// Un nome tmux e' "safe" se non contiene delimitatori che tmux rifiuta,
|
|
81
|
+
// interpreta o normalizza. L'unico raggiungibile via CELL_ID_RE/TMUX_NAME_RE e'
|
|
82
|
+
// il punto ('.' -> '_' silenzioso, poi trattato come separatore pane nei target).
|
|
83
|
+
// `:` non e' ammesso dai RE, e validTmuxName rifiuta gia' il leading '-': per
|
|
84
|
+
// costruzione il punto e' dunque l'unico carattere ostile coperto da questo gate.
|
|
85
|
+
function isTmuxSafeName(name) {
|
|
86
|
+
return validTmuxName(name) && TMUX_SAFE_NAME_RE.test(name);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Deriva il nome sessione tmux CANONICO di una cella. Puro, iniettivo, reversibile.
|
|
90
|
+
// - ID senza punto: storico `cloud-<id>` (le sessioni esistenti non vengono
|
|
91
|
+
// rinominate; il namespace e' tmux-safe perche' l'id stesso non ha punto).
|
|
92
|
+
// - ID con punto: mapping v2 dot-free (design §3.1.5):
|
|
93
|
+
// raw = base64url(UTF-8 id), senza padding '='
|
|
94
|
+
// n = lunghezza di raw su due cifre
|
|
95
|
+
// padded = raw right-padded con '-' fino a 43 caratteri
|
|
96
|
+
// session= "cloud-v2-" + n + "-" + padded (55 char, charset [A-Za-z0-9_-])
|
|
97
|
+
// Il suffisso v2 supera sempre i 32 char di un id, quindi il namespace e'
|
|
98
|
+
// disgiunto da `cloud-<id>`. base64url di un id ASCII <=32 byte e' <=43 char,
|
|
99
|
+
// cosi' `padded` non tronca mai. Ritorna null se l'id non e' un cell id valido.
|
|
100
|
+
function tmuxSessionForCell(cellId) {
|
|
101
|
+
if (typeof cellId !== 'string' || !CELL_ID_RE.test(cellId)) return null;
|
|
102
|
+
if (!cellId.includes('.')) return `cloud-${cellId}`;
|
|
103
|
+
const raw = Buffer.from(cellId, 'utf8').toString('base64url'); // base64url: nessun padding '='
|
|
104
|
+
const n = String(raw.length).padStart(2, '0').slice(-2);
|
|
105
|
+
const padded = (raw + '-'.repeat(43)).slice(0, 43);
|
|
106
|
+
return `cloud-v2-${n}-${padded}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Reverse di tmuxSessionForCell: dal nome sessione tmux ricostruisce il cellId
|
|
110
|
+
// umano quando il nome e' canonico (v2 decodificato, o cloud-<id> senza punto).
|
|
111
|
+
// Verifica il round-trip esatto (tmuxSessionForCell(id) === session) per evitare
|
|
112
|
+
// falsi positivi. Ritorna null per nomi non canonici (es. override custom, "jarvis").
|
|
113
|
+
function cellIdFromTmuxSession(session) {
|
|
114
|
+
if (typeof session !== 'string' || !session) return null;
|
|
115
|
+
const m = /^cloud-v2-(\d{2})-(.{43})$/.exec(session);
|
|
116
|
+
if (m) {
|
|
117
|
+
const n = Number(m[1]);
|
|
118
|
+
if (Number.isInteger(n) && n >= 1 && n <= 43) {
|
|
119
|
+
try {
|
|
120
|
+
const id = Buffer.from(m[2].slice(0, n), 'base64url').toString('utf8');
|
|
121
|
+
if (CELL_ID_RE.test(id) && tmuxSessionForCell(id) === session) return id;
|
|
122
|
+
} catch (_) { /* fall through */ }
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
if (session.startsWith('cloud-')) {
|
|
127
|
+
const suffix = session.slice('cloud-'.length);
|
|
128
|
+
if (CELL_ID_RE.test(suffix) && !suffix.includes('.') && tmuxSessionForCell(suffix) === session) return suffix;
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
79
133
|
// ---------------------------------------------------------------------------
|
|
80
134
|
// parseDefinitions(raw) -> {schemaVersion, engines, cells} | null
|
|
81
135
|
// Accetta stringa JSON o oggetto gia' parsato. Strict + fail-closed.
|
|
82
136
|
// ---------------------------------------------------------------------------
|
|
83
|
-
function parseDefinitions(raw) {
|
|
137
|
+
function parseDefinitions(raw, { allowLegacyTmuxNames = true } = {}) {
|
|
84
138
|
try {
|
|
85
139
|
let d;
|
|
86
140
|
if (typeof raw === 'string') {
|
|
@@ -110,19 +164,32 @@ function parseDefinitions(raw) {
|
|
|
110
164
|
}
|
|
111
165
|
|
|
112
166
|
const tmuxSeen = new Set();
|
|
167
|
+
const legacyTmuxSeen = new Set();
|
|
168
|
+
const legacyTmuxSessions = new Map();
|
|
113
169
|
const cellIds = new Set();
|
|
114
170
|
const cells = [];
|
|
115
171
|
for (const c of d.cells) {
|
|
116
|
-
const cell = parseCell(c, engineIds, engineMap);
|
|
172
|
+
const cell = parseCell(c, engineIds, engineMap, { allowLegacyTmuxNames });
|
|
117
173
|
if (!cell) return null;
|
|
118
174
|
if (cellIds.has(cell.id)) return null; // id cell univoco
|
|
119
175
|
cellIds.add(cell.id);
|
|
120
176
|
if (tmuxSeen.has(cell.tmuxSession)) return null; // tmuxSession univoco
|
|
121
177
|
tmuxSeen.add(cell.tmuxSession);
|
|
178
|
+
if (cell.legacyTmuxSession) {
|
|
179
|
+
if (legacyTmuxSeen.has(cell.legacyTmuxSession)) return null;
|
|
180
|
+
legacyTmuxSeen.add(cell.legacyTmuxSession);
|
|
181
|
+
legacyTmuxSessions.set(cell.id, cell.legacyTmuxSession);
|
|
182
|
+
}
|
|
122
183
|
cells.push(cell);
|
|
123
184
|
}
|
|
124
185
|
|
|
125
|
-
|
|
186
|
+
const parsed = { schemaVersion: SCHEMA_VERSION, engines, cells };
|
|
187
|
+
// Metadato solo in memoria: serve al bootstrap per rinominare una sessione
|
|
188
|
+
// legacy PRIMA di persistere il nome safe. Non entra in JSON, draft o API.
|
|
189
|
+
Object.defineProperty(parsed, 'legacyTmuxSessions', {
|
|
190
|
+
value: legacyTmuxSessions, enumerable: false, configurable: false,
|
|
191
|
+
});
|
|
192
|
+
return parsed;
|
|
126
193
|
} catch (_) {
|
|
127
194
|
return null; // fail-closed: qualunque eccezione inattesa -> null, MAI throw
|
|
128
195
|
}
|
|
@@ -218,7 +285,7 @@ function parseEngine(e) {
|
|
|
218
285
|
return out;
|
|
219
286
|
}
|
|
220
287
|
|
|
221
|
-
function parseCell(c, engineIds, engineMap = new Map()) {
|
|
288
|
+
function parseCell(c, engineIds, engineMap = new Map(), { allowLegacyTmuxNames = true } = {}) {
|
|
222
289
|
if (!c || typeof c !== 'object' || Array.isArray(c)) return null;
|
|
223
290
|
|
|
224
291
|
// id
|
|
@@ -312,17 +379,38 @@ function parseCell(c, engineIds, engineMap = new Map()) {
|
|
|
312
379
|
}
|
|
313
380
|
|
|
314
381
|
// tmuxSession: campo esplicito o derivato da id. UNIVOCO (check in caller).
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
|
|
382
|
+
// Il nome CANONICO e' tmux-safe (v2 per id puntati): tmux normalizza '.' in
|
|
383
|
+
// '_' nei nomi sessione, per cui `cloud-agy.native` diverrebbe `cloud-agy_native`
|
|
384
|
+
// e ogni target `-t =cloud-agy.native:` fallirebbe deterministicamente.
|
|
385
|
+
// - override === canonico safe (post-migrazione): round-trip.
|
|
386
|
+
// - qualunque override legacy con punto e' ammesso SOLO dal percorso di
|
|
387
|
+
// lettura/migrazione, normalizzato in memoria al canonico safe e conservato
|
|
388
|
+
// come metadato non enumerabile. atomicWrite usa il parser strict e quindi
|
|
389
|
+
// non puo reintrodurlo come nuovo valore.
|
|
390
|
+
// - override cloud-* di altra cella: rifiutato (aliaserebbe sessioni altrui).
|
|
391
|
+
// - override custom con punto: rifiutato (nuovo nome non tmux-safe).
|
|
392
|
+
// - override custom senza punto: ammesso (gia' tmux-safe).
|
|
393
|
+
// La restrizione del punto vive in SCRITTURA (derivazione/override), non in
|
|
394
|
+
// lettura: validTmuxName resta permissivo, cosi' uno store legacy con
|
|
395
|
+
// tmuxSession puntato viene normalizzato, non scartato.
|
|
396
|
+
const canonical = tmuxSessionForCell(c.id);
|
|
397
|
+
const legacy = `cloud-${c.id}`;
|
|
321
398
|
let tmuxSession;
|
|
399
|
+
let legacyTmuxSession = '';
|
|
322
400
|
if (c.tmuxSession !== undefined) {
|
|
323
401
|
if (typeof c.tmuxSession !== 'string' || !validTmuxName(c.tmuxSession)) return null;
|
|
324
|
-
if (
|
|
325
|
-
|
|
402
|
+
if (c.tmuxSession.includes('.')) {
|
|
403
|
+
if (!allowLegacyTmuxNames) return null;
|
|
404
|
+
if (/^cloud-/i.test(c.tmuxSession) && c.tmuxSession !== legacy) return null;
|
|
405
|
+
legacyTmuxSession = c.tmuxSession;
|
|
406
|
+
tmuxSession = canonical;
|
|
407
|
+
} else if (c.tmuxSession === canonical || c.tmuxSession === legacy) {
|
|
408
|
+
tmuxSession = canonical;
|
|
409
|
+
} else if (/^cloud-/i.test(c.tmuxSession)) {
|
|
410
|
+
return null; // alias cloud-* di altra cella
|
|
411
|
+
} else {
|
|
412
|
+
tmuxSession = c.tmuxSession; // override custom tmux-safe (no punto)
|
|
413
|
+
}
|
|
326
414
|
} else {
|
|
327
415
|
tmuxSession = canonical;
|
|
328
416
|
}
|
|
@@ -334,6 +422,11 @@ function parseCell(c, engineIds, engineMap = new Map()) {
|
|
|
334
422
|
if (permissionPolicies) out.permissionPolicies = permissionPolicies;
|
|
335
423
|
if (commands && Object.keys(commands).length) out.commands = commands;
|
|
336
424
|
if (prompt !== undefined) out.prompt = prompt;
|
|
425
|
+
if (legacyTmuxSession) {
|
|
426
|
+
Object.defineProperty(out, 'legacyTmuxSession', {
|
|
427
|
+
value: legacyTmuxSession, enumerable: false, configurable: false,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
337
430
|
return out;
|
|
338
431
|
}
|
|
339
432
|
|
|
@@ -469,7 +562,9 @@ function atomicWrite(p, data) {
|
|
|
469
562
|
else throw e; // inclusi i nostri 'refuse to write'
|
|
470
563
|
}
|
|
471
564
|
|
|
472
|
-
|
|
565
|
+
// Le letture devono accettare store legacy per poterli migrare; le scritture
|
|
566
|
+
// invece sono sempre tmux-safe e rifiutano qualunque nuovo nome con punto.
|
|
567
|
+
const parsed = parseDefinitions(data, { allowLegacyTmuxNames: false });
|
|
473
568
|
if (!parsed) {
|
|
474
569
|
backupPredecessor(p); // conserva il precedente per recovery/forensics
|
|
475
570
|
throw new Error('definizioni fleet non valide: validazione fallita');
|
|
@@ -505,6 +600,9 @@ module.exports = {
|
|
|
505
600
|
loadDefinitions,
|
|
506
601
|
atomicWrite,
|
|
507
602
|
validTmuxName,
|
|
603
|
+
isTmuxSafeName,
|
|
604
|
+
tmuxSessionForCell,
|
|
605
|
+
cellIdFromTmuxSession,
|
|
508
606
|
CAPS,
|
|
509
607
|
// Costanti esposte anche piatte (comode per la UI/schema e i test)
|
|
510
608
|
SCHEMA_VERSION, MAX_ENGINES, MAX_CELLS, MAX_ARGS, MAX_ARG_LEN,
|
|
@@ -130,7 +130,10 @@ function createLaunchBroker(cfg = {}) {
|
|
|
130
130
|
try { if (socketPath) fs.unlinkSync(socketPath); } catch (_) {}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
// Revoca esplicita di un nonce pendente (design §3.3): se il respawn-pane
|
|
134
|
+
// fallisce dopo issue(), il runtime consuma/revoca il ticket subito invece di
|
|
135
|
+
// attenderne il TTL. expire() e' gia' no-op su nonce mancante/scaduto.
|
|
136
|
+
return { issue, close, revoke: expire, pendingCount: () => pending.size };
|
|
134
137
|
}
|
|
135
138
|
|
|
136
139
|
module.exports = { createLaunchBroker, runtimeDir, ensureRuntimeDir, encodePayload, MAX_PAYLOAD };
|
package/lib/fleet/launch.js
CHANGED
|
@@ -23,6 +23,7 @@ const path = require('node:path');
|
|
|
23
23
|
const { execFile } = require('node:child_process');
|
|
24
24
|
const { minimalRuntimeEnv } = require('../runtime/env.js');
|
|
25
25
|
const { codeOf, phaseOf } = require('./causes.js');
|
|
26
|
+
const { isTmuxSafeName, tmuxSessionForCell } = require('./definitions.js');
|
|
26
27
|
|
|
27
28
|
// Env minimale controllato dal service (design §9a). Allowlist DURA: le definizioni
|
|
28
29
|
// non possono toccare PATH/loader-key (parseDefinitions le rifiuta gia' in env);
|
|
@@ -129,6 +130,128 @@ function tmuxExec(tmuxBin, args, { env, timeoutMs = 10000 } = {}) {
|
|
|
129
130
|
|
|
130
131
|
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
|
131
132
|
|
|
133
|
+
function tmuxMigrationError(code, message) {
|
|
134
|
+
const error = new Error(message);
|
|
135
|
+
error.code = code;
|
|
136
|
+
return error;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function boundedTmuxFailure(result) {
|
|
140
|
+
return String(result?.stderr || result?.err?.message || 'errore tmux')
|
|
141
|
+
.replace(/[\x00-\x1f\x7f]+/g, ' ').trim().slice(0, 240) || 'errore tmux';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Migrazione identita' tmux (design §3.2). Prima costruisce e valida TUTTO il
|
|
145
|
+
// piano: nessun rename parte se una sessione legacy normalizzata e' rivendicata
|
|
146
|
+
// da piu celle (es. a.b vs a_b), se legacy+target safe coesistono o se il target
|
|
147
|
+
// non e' tmux-safe. Solo dopo il preflight rinomina via `$N`; qualunque errore e'
|
|
148
|
+
// propagato con causa bounded e impedisce la persistenza di fleet.json. I rename
|
|
149
|
+
// gia riusciti prima di un errore operativo restano comunque idempotenti al boot
|
|
150
|
+
// successivo, mentre lo store precedente rimane intatto.
|
|
151
|
+
async function migrateLegacyTmuxSessions(tmuxBin, defs, { readonly = false } = {}) {
|
|
152
|
+
const cells = defs && Array.isArray(defs.cells) ? defs.cells : [];
|
|
153
|
+
const legacyMap = defs?.legacyTmuxSessions instanceof Map ? defs.legacyTmuxSessions : new Map();
|
|
154
|
+
const candidates = [];
|
|
155
|
+
for (const cell of cells) {
|
|
156
|
+
if (!cell || typeof cell.id !== 'string') continue;
|
|
157
|
+
const legacyRaw = legacyMap.get(cell.id) || (cell.id.includes('.') ? `cloud-${cell.id}` : '');
|
|
158
|
+
if (!legacyRaw) continue;
|
|
159
|
+
const safe = cell.tmuxSession || tmuxSessionForCell(cell.id);
|
|
160
|
+
if (!isTmuxSafeName(safe)) {
|
|
161
|
+
throw tmuxMigrationError('TMUX_MIGRATION_INVALID_TARGET',
|
|
162
|
+
`migrazione tmux bloccata per ${cell.id}: target safe non valido`);
|
|
163
|
+
}
|
|
164
|
+
candidates.push({ id: cell.id, legacyRaw, legacyNorm: legacyRaw.replace(/\./g, '_'), safe });
|
|
165
|
+
}
|
|
166
|
+
if (!candidates.length || readonly) {
|
|
167
|
+
return {
|
|
168
|
+
migrated: [], reason: readonly ? 'readonly' : 'none',
|
|
169
|
+
needsPersistence: legacyMap.size > 0,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const listing = await tmuxExec(tmuxBin,
|
|
173
|
+
['list-sessions', '-F', '#{session_id}\t#{session_name}'], { env: minimalEnv() });
|
|
174
|
+
if (listing.err) {
|
|
175
|
+
const detail = boundedTmuxFailure(listing);
|
|
176
|
+
if (/no server running|failed to connect|connection refused|no such file.*tmux/i.test(detail)) {
|
|
177
|
+
return { migrated: [], reason: 'no-tmux-server', needsPersistence: legacyMap.size > 0 };
|
|
178
|
+
}
|
|
179
|
+
throw tmuxMigrationError('TMUX_MIGRATION_LIST_FAILED',
|
|
180
|
+
`migrazione tmux: elenco sessioni non disponibile (${detail})`);
|
|
181
|
+
}
|
|
182
|
+
const realByName = new Map(); // session_name -> session_id ($N)
|
|
183
|
+
for (const line of listing.stdout.split('\n')) {
|
|
184
|
+
const [sid, sname] = line.split('\t');
|
|
185
|
+
if (sid && sname) realByName.set(String(sname).trim(), String(sid).trim());
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const ownersBySafe = new Map(cells.map((cell) => [cell.tmuxSession, cell.id]));
|
|
189
|
+
const claimsByReal = new Map();
|
|
190
|
+
for (const candidate of candidates) {
|
|
191
|
+
for (const name of new Set([candidate.legacyRaw, candidate.legacyNorm])) {
|
|
192
|
+
if (!realByName.has(name)) continue;
|
|
193
|
+
const claims = claimsByReal.get(name) || [];
|
|
194
|
+
claims.push(candidate);
|
|
195
|
+
claimsByReal.set(name, claims);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const activeNamesByCell = new Map();
|
|
200
|
+
for (const [realName, claims] of claimsByReal) {
|
|
201
|
+
for (const claim of claims) {
|
|
202
|
+
const names = activeNamesByCell.get(claim.id) || [];
|
|
203
|
+
names.push(realName);
|
|
204
|
+
activeNamesByCell.set(claim.id, names);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
for (const [cellId, names] of activeNamesByCell) {
|
|
208
|
+
if (names.length > 1) {
|
|
209
|
+
throw tmuxMigrationError('TMUX_MIGRATION_AMBIGUOUS',
|
|
210
|
+
`migrazione tmux ambigua per ${cellId}: piu sessioni legacy (${names.join(', ')})`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const operations = [];
|
|
215
|
+
for (const [realName, claims] of claimsByReal) {
|
|
216
|
+
if (claims.length !== 1) {
|
|
217
|
+
throw tmuxMigrationError('TMUX_MIGRATION_AMBIGUOUS',
|
|
218
|
+
`migrazione tmux ambigua: ${realName} corrisponde a ${claims.map((c) => c.id).join(', ')}`);
|
|
219
|
+
}
|
|
220
|
+
const candidate = claims[0];
|
|
221
|
+
const owner = ownersBySafe.get(realName);
|
|
222
|
+
if (owner && owner !== candidate.id) {
|
|
223
|
+
throw tmuxMigrationError('TMUX_MIGRATION_AMBIGUOUS',
|
|
224
|
+
`migrazione tmux ambigua: ${realName} e gia assegnata a ${owner}`);
|
|
225
|
+
}
|
|
226
|
+
const legacyId = realByName.get(realName);
|
|
227
|
+
if (!/^\$[0-9]+$/.test(legacyId || '')) {
|
|
228
|
+
throw tmuxMigrationError('TMUX_MIGRATION_INVALID_ID',
|
|
229
|
+
`migrazione tmux bloccata per ${candidate.id}: session ID non valido`);
|
|
230
|
+
}
|
|
231
|
+
const safeId = realByName.get(candidate.safe);
|
|
232
|
+
if (safeId && safeId !== legacyId) {
|
|
233
|
+
throw tmuxMigrationError('TMUX_MIGRATION_TARGET_EXISTS',
|
|
234
|
+
`migrazione tmux bloccata per ${candidate.id}: target ${candidate.safe} gia esistente`);
|
|
235
|
+
}
|
|
236
|
+
operations.push({ ...candidate, legacyId, realName });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const migrated = [];
|
|
240
|
+
for (const operation of operations) {
|
|
241
|
+
const rn = await tmuxExec(tmuxBin,
|
|
242
|
+
['rename-session', '-t', operation.legacyId, operation.safe], { env: minimalEnv() });
|
|
243
|
+
if (rn.err) {
|
|
244
|
+
throw tmuxMigrationError('TMUX_MIGRATION_RENAME_FAILED',
|
|
245
|
+
`migrazione tmux fallita per ${operation.id} (${boundedTmuxFailure(rn)})`);
|
|
246
|
+
}
|
|
247
|
+
migrated.push({ id: operation.id, from: operation.realName, to: operation.safe });
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
migrated, reason: 'ok',
|
|
251
|
+
needsPersistence: legacyMap.size > 0 || migrated.length > 0,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
132
255
|
// Policy caratteri del prompt send-keys (§9e): ammette stampabili + \t \n \r;
|
|
133
256
|
// rifiuta ESC(0x1b) e gli altri byte di controllo (niente marker bracketed-paste
|
|
134
257
|
// iniettabili). parseDefinitions caps solo la lunghezza: questo e' defense-in-depth.
|
|
@@ -243,6 +366,7 @@ module.exports = {
|
|
|
243
366
|
promptCharsOk,
|
|
244
367
|
composeClientInvocation,
|
|
245
368
|
composeLaunchArgv,
|
|
369
|
+
migrateLegacyTmuxSessions,
|
|
246
370
|
waitAlive,
|
|
247
371
|
waitStablePane,
|
|
248
372
|
injectPrompt,
|
package/lib/fleet/managed.js
CHANGED
|
@@ -61,7 +61,7 @@ const ALIBABA_PI_MODELS = Object.freeze([
|
|
|
61
61
|
|
|
62
62
|
const CUSTOM_KEYS = ['displayName', 'protocol', 'baseUrl', 'envKey', 'providerId'];
|
|
63
63
|
const MANAGED_KEYS = new Set(['client', 'provider', 'credentialProfile', 'model', 'permissionPolicy', ...CUSTOM_KEYS]);
|
|
64
|
-
const CLIENT_LABELS = Object.freeze({ claude: 'Claude Code', codex: 'Codex', 'codex-vl': 'Codex-VL', pi: 'Pi', shell: 'Shell' });
|
|
64
|
+
const CLIENT_LABELS = Object.freeze({ claude: 'Claude Code', codex: 'Codex', 'codex-vl': 'Codex-VL', pi: 'Pi', agy: 'Agy', shell: 'Shell' });
|
|
65
65
|
const PROVIDER_ID_RE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
66
66
|
|
|
67
67
|
function validBaseUrl(value) {
|
|
@@ -128,6 +128,16 @@ const CATALOG = Object.freeze([
|
|
|
128
128
|
{ id: 'pi.ollama', client: 'pi', provider: 'ollama', label: 'Ollama local', auth: 'none', protocol: 'openai-completions', piProvider: 'ollama', requiresModel: true, core: true, piExtension: { baseUrl: 'http://127.0.0.1:11434/v1', apiKey: 'ollama' } },
|
|
129
129
|
{ id: 'pi.zai', client: 'pi', provider: 'zai', label: 'Z.AI', auth: 'ZAI_API_KEY', protocol: 'pi_native', piProvider: 'zai', core: true },
|
|
130
130
|
{ id: 'pi.custom', client: 'pi', provider: 'custom', label: 'Custom provider', auth: 'dynamic', protocol: 'openai-responses', protocols: ['openai-responses', 'anthropic-messages', 'openai-completions', 'google-generative-ai'], custom: true, core: true },
|
|
131
|
+
|
|
132
|
+
// Agy: client gestito primario (Linux/macOS non-Termux). Auth delegata al
|
|
133
|
+
// login locale del client (nessuno store letto/copiato). Nessun remote-control
|
|
134
|
+
// NexusCrew; permission default 'standard' (unsafe aggiunge --dangerously-skip-
|
|
135
|
+
// permissions, allineato agli altri client). NON e' un default seed (non va nel
|
|
136
|
+
// fleet.json fresco su piattaforme non supportate): viene aggiunto solo da un
|
|
137
|
+
// backfill platform-aware (builtin.js) su Linux/macOS non-Termux. Qui figura
|
|
138
|
+
// (core) per la UI; describeManaged lo dichiara non configurato altrove.
|
|
139
|
+
// Termux/Windows restano fuori dal primary: l'utente usa agy via shell.local.
|
|
140
|
+
{ id: 'agy.native', client: 'agy', provider: 'native', label: 'Agy', auth: 'login', protocol: 'agy_native', core: true },
|
|
131
141
|
]);
|
|
132
142
|
|
|
133
143
|
function profileFor(client, provider, credentialProfile) {
|
|
@@ -202,6 +212,18 @@ function defaultShellEngine() {
|
|
|
202
212
|
};
|
|
203
213
|
}
|
|
204
214
|
|
|
215
|
+
// Engine Agy per il backfill platform-aware (builtin.js): standard di default,
|
|
216
|
+
// auth delegata al login locale, niente remote-control. Non e' un default seed.
|
|
217
|
+
function defaultAgyEngine() {
|
|
218
|
+
const profile = CATALOG.find((entry) => entry.id === 'agy.native');
|
|
219
|
+
return {
|
|
220
|
+
id: profile.id,
|
|
221
|
+
label: CLIENT_LABELS.agy,
|
|
222
|
+
rc: false,
|
|
223
|
+
managed: { client: 'agy', provider: 'native', model: '', permissionPolicy: 'standard' },
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
205
227
|
function parseAssignments(raw) {
|
|
206
228
|
const out = {};
|
|
207
229
|
for (const line of raw.split(/\r?\n/)) {
|
|
@@ -485,17 +507,30 @@ function describeManaged(spec, cfg = {}) {
|
|
|
485
507
|
const delegatedPiAuth = profile.client === 'pi' && profile.provider !== 'custom'
|
|
486
508
|
&& profile.delegatePiAuth !== false;
|
|
487
509
|
const authConfigured = delegatedPiAuth || profile.auth === 'login' || profile.auth === 'none' || !!cred.value;
|
|
510
|
+
let configured = !!binary && authConfigured;
|
|
511
|
+
let reason = !binary ? `client ${profile.client} not found` : (!authConfigured
|
|
512
|
+
? `credential ${cred.envKey} missing — set it on this device`
|
|
513
|
+
: 'ready');
|
|
514
|
+
// Agy e' un client primario supportato solo su Linux/macOS non-Termux.
|
|
515
|
+
// Rilevazione Termux via termuxRuntimePaths (non solo process.platform): un
|
|
516
|
+
// Node che riporta 'linux' sotto proot/Termux viene comunque intercettato.
|
|
517
|
+
if (normalized.client === 'agy') {
|
|
518
|
+
const platform = cfg.platform || process.platform;
|
|
519
|
+
const termux = platform === 'android' || termuxRuntimePaths(cfg.env || process.env, { platform, home }) !== null;
|
|
520
|
+
if (termux || (platform !== 'linux' && platform !== 'darwin')) {
|
|
521
|
+
configured = false;
|
|
522
|
+
reason = 'agy non supportato su questa piattaforma (usa shell.local con command agy)';
|
|
523
|
+
}
|
|
524
|
+
}
|
|
488
525
|
return {
|
|
489
526
|
client: profile.client, clientLabel: CLIENT_LABELS[profile.client], provider: profile.provider,
|
|
490
527
|
credentialProfile: normalized.credentialProfile || '', model: normalized.model,
|
|
491
528
|
permissionPolicy: normalized.permissionPolicy, protocol: normalized.protocol || profile.protocol,
|
|
492
529
|
endpoint: normalized.baseUrl || profile.endpoint || '', auth: cred.envKey, authConfigured,
|
|
493
530
|
credentialSource: authConfigured ? cred.source : 'missing',
|
|
494
|
-
configured
|
|
531
|
+
configured, models: [...(profile.models || [])], defaultModel: profile.model || '',
|
|
495
532
|
binary: binary || '', displayName: normalized.displayName || profile.label,
|
|
496
|
-
reason
|
|
497
|
-
? `credential ${cred.envKey} missing — set it on this device`
|
|
498
|
-
: 'ready'),
|
|
533
|
+
reason,
|
|
499
534
|
};
|
|
500
535
|
}
|
|
501
536
|
|
|
@@ -644,7 +679,7 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
644
679
|
if (spec.client === 'pi' || spec.client === 'shell') effectivePolicy = 'standard';
|
|
645
680
|
info.permissionPolicy = effectivePolicy;
|
|
646
681
|
if (effectivePolicy === 'unsafe') {
|
|
647
|
-
if (spec.client === 'claude') args.push('--dangerously-skip-permissions');
|
|
682
|
+
if (spec.client === 'claude' || spec.client === 'agy') args.push('--dangerously-skip-permissions');
|
|
648
683
|
if (spec.client === 'codex' || spec.client === 'codex-vl') args.push('--dangerously-bypass-approvals-and-sandbox');
|
|
649
684
|
}
|
|
650
685
|
let shellOneShot = false;
|
|
@@ -770,6 +805,13 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
770
805
|
if (spec.provider !== 'native') args.push('--provider', spec.provider === 'custom' ? spec.providerId : profile.piProvider);
|
|
771
806
|
if (model) args.push('--model', model);
|
|
772
807
|
if (spec.provider === 'alibaba-token-plan' && model === 'qwen3.8-max-preview') args.push('--thinking', 'xhigh');
|
|
808
|
+
} else if (spec.client === 'agy') {
|
|
809
|
+
// Agy delega l'autenticazione al proprio login locale (auth: 'login'): niente
|
|
810
|
+
// env provider, niente credenziali copiate. Argv diretto: --model prima del
|
|
811
|
+
// prompt; --prompt-interactive e' l'ultima coppia (il prompt value e' accodato
|
|
812
|
+
// dal push generico qui sotto). Senza prompt parte il TUI interattivo `agy`.
|
|
813
|
+
if (model) args.push('--model', model);
|
|
814
|
+
if (cell?.prompt) args.push('--prompt-interactive');
|
|
773
815
|
}
|
|
774
816
|
if (spec.client !== 'shell' && cell?.prompt) args.push(cell.prompt);
|
|
775
817
|
let command = info.binary;
|
|
@@ -800,7 +842,7 @@ module.exports = {
|
|
|
800
842
|
CATALOG, OLLAMA_CLOUD_MODELS, OLLAMA_CONTEXT, ALIBABA_TOKEN_PLAN_MODELS,
|
|
801
843
|
ALIBABA_CODEX_MODELS, ALIBABA_TOKEN_PLAN_CONTEXT, ALIBABA_PI_MODELS,
|
|
802
844
|
CLIENT_LABELS, normalizeManagedSpec,
|
|
803
|
-
defaultDefinitions, defaultShellEngine, describeManaged, describeCatalogCredential, discoverOllamaModels, resolveManagedEngine, needsExplicitNode,
|
|
845
|
+
defaultDefinitions, defaultShellEngine, defaultAgyEngine, describeManaged, describeCatalogCredential, discoverOllamaModels, resolveManagedEngine, needsExplicitNode,
|
|
804
846
|
discoverPiModels, parseEnvFile, parseProviderShellFile, findBinary, publicCatalog, writePiProviderExtension,
|
|
805
847
|
providerKeyPaths, parseProviderKeyFiles, credentialSources, credential,
|
|
806
848
|
ensureKimiClaudeConfig, ensureAlibabaClaudeConfig, resolveInteractiveShell,
|