@mmmbuto/nexuscrew 0.8.5 → 0.8.6
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/README.md +33 -16
- package/frontend/dist/assets/index-CEFh0hI4.js +90 -0
- package/frontend/dist/assets/{index-U_e8auCM.css → index-kX5IUxt4.css} +1 -1
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/cli/fleet-service.js +8 -3
- package/lib/fleet/builtin.js +139 -22
- package/lib/fleet/definitions.js +20 -0
- package/lib/fleet/index.js +42 -1
- package/lib/fleet/managed.js +15 -2
- package/lib/fleet/provider.js +12 -11
- package/lib/fleet/routes.js +11 -2
- package/lib/nodes/peering.js +59 -7
- package/lib/settings/routes.js +22 -2
- package/package.json +1 -1
- package/frontend/dist/assets/index-UAVxf6SE.js +0 -90
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
.nc-icon{display:block;flex:0 0 auto}.nc-sheet-overlay{position:fixed;top:0;right:0;bottom:0;left:0;z-index:50;background:#0000008c;display:flex;align-items:center;justify-content:center;padding:16px;box-sizing:border-box}.nc-sheet{width:100%;max-width:380px;background:#0e140e;color:#cfe;border:1px solid #243;border-radius:14px;padding:16px;display:flex;flex-direction:column;gap:12px;box-shadow:0 12px 40px #00000080}.nc-power-sheet{max-width:min(620px,calc(100vw - 32px));max-height:calc(100dvh - 32px);overflow-y:auto;box-sizing:border-box;min-width:0}.nc-sheet-head{display:flex;align-items:center;justify-content:space-between}.nc-sheet-head b{font-size:16px;color:#d8ffe8}.nc-sheet-state{font-size:12px;opacity:.6}.nc-sheet-label{font-size:12px;opacity:.7;text-transform:uppercase;letter-spacing:.5px}.nc-power-config{display:grid;grid-template-columns:auto minmax(0,1fr);gap:4px 10px;padding:10px;border:1px solid #243;border-radius:8px;background:#0a0e0a;min-width:0;overflow-wrap:anywhere}.nc-power-config span{opacity:.7}.nc-power-config small{grid-column:1 / -1;opacity:.65}.nc-check{display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer}.nc-check input{accent-color:#2e7d32}.nc-note{font-size:11px;color:#e0b020;opacity:.9}.nc-field{display:flex;flex-direction:column;gap:4px;font-size:12px;opacity:.8}.nc-field input,.nc-field select{padding:9px 10px;font:14px/1.2 inherit;background:#0a0e0a;color:#cfe;border:1px solid #243;border-radius:8px;width:100%;min-width:0;max-width:100%;box-sizing:border-box}.nc-field input:focus,.nc-field select:focus{outline:none;border-color:#2e7d32}.nc-err{color:#f88;font-size:12px}.nc-sheet-actions{display:flex;justify-content:flex-end;gap:8px;flex-wrap:wrap;min-width:0}.nc-btn{border:none;border-radius:8px;padding:8px 16px;font-size:13px;cursor:pointer}.nc-btn.ghost{background:none;border:1px solid #243;color:#9fd}.nc-btn.primary{background:#2e7d32;color:#fff}.nc-btn.primary:active{background:#255028}.nc-btn:disabled{opacity:.5;cursor:default}@media(max-width:600px){.nc-power-sheet{max-width:380px}}.nc-home{display:flex;flex-direction:column;gap:12px;padding:18px 14px 10px;color:#cfe;max-width:560px;margin:0 auto;min-height:100dvh;box-sizing:border-box}.nc-home-head{position:relative}.nc-wordmark{font-size:26px;font-weight:700;letter-spacing:.5px;color:#d8ffe8}.nc-cursor{display:inline-block;width:12px;height:22px;margin-left:6px;background:#0c6;vertical-align:-2px;animation:nc-blink 1.1s steps(1) infinite}@keyframes nc-blink{50%{opacity:0}}@media(prefers-reduced-motion:reduce){.nc-cursor{animation:none}}.nc-home-sub{margin-top:4px;font-size:13px;opacity:.65}.nc-head-actions{position:absolute;top:0;right:0;display:inline-flex;gap:6px}.nc-refresh{display:inline-flex;align-items:center;justify-content:center;background:none;border:1px solid #243;color:#9fd;border-radius:8px;min-width:40px;min-height:40px}.nc-filter{padding:10px 12px;font:16px/1.2 inherit;background:#0e140e;color:#cfe;border:1px solid #243;border-radius:8px}.nc-filter:focus{outline:none;border-color:#2e7d32}.nc-session-list{display:flex;flex-direction:column;gap:8px}.nc-session{display:flex;align-items:center;gap:12px;padding:12px 14px;border:1px solid #1c241c;background:#0e140e;color:#cfe;border-radius:10px;text-align:left;min-height:56px}.nc-session:active{background:#14200f;border-color:#2e7d32}.nc-session .dot{flex:0 0 auto;width:9px;height:9px;border-radius:50%;background:#355}.nc-session .dot.on{background:#0c6;box-shadow:0 0 6px #0c68}.nc-session-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.nc-session-main b{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px}.nc-session-main small{opacity:.55;font-size:12px}.nc-badge{flex:0 0 auto;background:#2e7d32;color:#fff;border-radius:10px;padding:1px 8px;font-size:12px;line-height:18px}.nc-err{color:#f88;font-size:13px;padding:4px 2px}.nc-empty{opacity:.5;padding:18px 4px;font-size:13px}.nc-home-foot{margin-top:auto;padding-top:14px;display:flex;justify-content:space-between;gap:8px;font-size:11px;opacity:.4}.nc-group{display:flex;flex-direction:column;gap:8px}.nc-group-title{font-size:11px;opacity:.55;text-transform:uppercase;letter-spacing:.5px;padding:6px 2px 2px}.nc-mcard{display:flex;align-items:center;gap:12px;padding:12px 14px;border:1px solid #1c241c;background:#0e140e;color:#cfe;border-radius:12px;min-height:56px}.nc-mcard-main{flex:1 1 auto;min-width:0;display:flex;align-items:center;gap:12px;background:none;border:none;color:inherit;cursor:pointer;padding:0;text-align:left}.nc-mcard-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.nc-mcard-text b{font-size:14px;color:#d8ffe8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.nc-mcard-text small{font-size:12px;opacity:.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dot.warn{background:#e0b020;box-shadow:0 0 6px #e0b02088}.nc-group-title.nc-node-title{display:flex;align-items:center;gap:6px}.nc-group-title.nc-node-title .dot{flex:0 0 auto;width:8px;height:8px;border-radius:50%;background:#355}.nc-group-title.nc-node-title .dot.on{background:#0c6;box-shadow:0 0 6px #0c68}.nc-mcard .dot{flex:0 0 auto;width:9px;height:9px;border-radius:50%;background:#355}.nc-mcard .dot.on{background:#0c6;box-shadow:0 0 6px #0c68}.nc-rel{flex:0 0 auto;font-size:11px;opacity:.45}.nc-power,.nc-menu{flex:0 0 auto;background:none;border:1px solid #243;color:#9fd;border-radius:8px;min-width:38px;min-height:38px;font-size:17px;cursor:pointer}.nc-power:active,.nc-menu:active{border-color:#2e7d32}.nc-fab{position:fixed;right:18px;bottom:18px;z-index:20;width:54px;height:54px;border-radius:50%;background:#2e7d32;color:#fff;border:none;font-size:28px;line-height:1;cursor:pointer;box-shadow:0 6px 18px #00000073}.nc-fab:active{background:#255028}.nc-lang{display:inline-flex;align-items:center;gap:2px}.nc-act{width:34px;height:34px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:#ffffff0d;border:1px solid rgba(255,255,255,.22);color:#9aa89a;cursor:pointer;flex:0 0 auto;font-size:15px;line-height:1}.nc-act.power.on{color:#0c6;border-color:#00cc668c;background:#00cc661a}.nc-act.power.warn{color:#e8c547;border-color:#e8c5478c;background:#e8c5471a}.nc-act.pin.on{color:#e8c547;border-color:#e8c5478c;background:#e8c54714}.nc-mcard{gap:8px}/**
|
|
1
|
+
.nc-icon{display:block;flex:0 0 auto}.nc-sheet-overlay{position:fixed;top:0;right:0;bottom:0;left:0;z-index:50;background:#0000008c;display:flex;align-items:center;justify-content:center;padding:16px;box-sizing:border-box}.nc-launch-overlay{z-index:70}.nc-sheet{width:100%;max-width:380px;background:#0e140e;color:#cfe;border:1px solid #243;border-radius:14px;padding:16px;display:flex;flex-direction:column;gap:12px;box-shadow:0 12px 40px #00000080}.nc-power-sheet{max-width:min(620px,calc(100vw - 32px));max-height:calc(100dvh - 32px);overflow-y:auto;box-sizing:border-box;min-width:0}.nc-launch-sheet{max-width:min(520px,calc(100vw - 32px));width:100%;min-width:0}.nc-launch-fields{display:flex;flex-direction:column;gap:10px;min-width:0}.nc-launch-fields .nc-field,.nc-launch-fields .nc-field input,.nc-launch-fields .nc-field select{width:100%;min-width:0;max-width:100%;box-sizing:border-box}.nc-sheet-head{display:flex;align-items:center;justify-content:space-between}.nc-sheet-head b{font-size:16px;color:#d8ffe8}.nc-sheet-state{font-size:12px;opacity:.6}.nc-sheet-label{font-size:12px;opacity:.7;text-transform:uppercase;letter-spacing:.5px}.nc-power-config{display:grid;grid-template-columns:auto minmax(0,1fr);gap:4px 10px;padding:10px;border:1px solid #243;border-radius:8px;background:#0a0e0a;min-width:0;overflow-wrap:anywhere}.nc-power-config span{opacity:.7}.nc-power-config small{grid-column:1 / -1;opacity:.65}.nc-check{display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer}.nc-check input{accent-color:#2e7d32}.nc-note{font-size:11px;color:#e0b020;opacity:.9}.nc-field{display:flex;flex-direction:column;gap:4px;font-size:12px;opacity:.8}.nc-field input,.nc-field select{padding:9px 10px;font:14px/1.2 inherit;background:#0a0e0a;color:#cfe;border:1px solid #243;border-radius:8px;width:100%;min-width:0;max-width:100%;box-sizing:border-box}.nc-field input:focus,.nc-field select:focus{outline:none;border-color:#2e7d32}.nc-err{color:#f88;font-size:12px}.nc-sheet-actions{display:flex;justify-content:flex-end;gap:8px;flex-wrap:wrap;min-width:0}.nc-btn{border:none;border-radius:8px;padding:8px 16px;font-size:13px;cursor:pointer}.nc-btn.ghost{background:none;border:1px solid #243;color:#9fd}.nc-btn.primary{background:#2e7d32;color:#fff}.nc-btn.primary:active{background:#255028}.nc-btn:disabled{opacity:.5;cursor:default}@media(max-width:600px){.nc-power-sheet{max-width:380px}}.nc-home{display:flex;flex-direction:column;gap:12px;padding:18px 14px 10px;color:#cfe;max-width:560px;margin:0 auto;min-height:100dvh;box-sizing:border-box}.nc-home-head{position:relative}.nc-wordmark{font-size:26px;font-weight:700;letter-spacing:.5px;color:#d8ffe8}.nc-cursor{display:inline-block;width:12px;height:22px;margin-left:6px;background:#0c6;vertical-align:-2px;animation:nc-blink 1.1s steps(1) infinite}@keyframes nc-blink{50%{opacity:0}}@media(prefers-reduced-motion:reduce){.nc-cursor{animation:none}}.nc-home-sub{margin-top:4px;font-size:13px;opacity:.65}.nc-head-actions{position:absolute;top:0;right:0;display:inline-flex;gap:6px}.nc-refresh{display:inline-flex;align-items:center;justify-content:center;background:none;border:1px solid #243;color:#9fd;border-radius:8px;min-width:40px;min-height:40px}.nc-filter{padding:10px 12px;font:16px/1.2 inherit;background:#0e140e;color:#cfe;border:1px solid #243;border-radius:8px}.nc-filter:focus{outline:none;border-color:#2e7d32}.nc-session-list{display:flex;flex-direction:column;gap:8px}.nc-session{display:flex;align-items:center;gap:12px;padding:12px 14px;border:1px solid #1c241c;background:#0e140e;color:#cfe;border-radius:10px;text-align:left;min-height:56px}.nc-session:active{background:#14200f;border-color:#2e7d32}.nc-session .dot{flex:0 0 auto;width:9px;height:9px;border-radius:50%;background:#355}.nc-session .dot.on{background:#0c6;box-shadow:0 0 6px #0c68}.nc-session-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.nc-session-main b{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px}.nc-session-main small{opacity:.55;font-size:12px}.nc-badge{flex:0 0 auto;background:#2e7d32;color:#fff;border-radius:10px;padding:1px 8px;font-size:12px;line-height:18px}.nc-err{color:#f88;font-size:13px;padding:4px 2px}.nc-empty{opacity:.5;padding:18px 4px;font-size:13px}.nc-home-foot{margin-top:auto;padding-top:14px;display:flex;justify-content:space-between;gap:8px;font-size:11px;opacity:.4}.nc-group{display:flex;flex-direction:column;gap:8px}.nc-group-title{font-size:11px;opacity:.55;text-transform:uppercase;letter-spacing:.5px;padding:6px 2px 2px}.nc-mcard{display:flex;align-items:center;gap:12px;padding:12px 14px;border:1px solid #1c241c;background:#0e140e;color:#cfe;border-radius:12px;min-height:56px}.nc-mcard-main{flex:1 1 auto;min-width:0;display:flex;align-items:center;gap:12px;background:none;border:none;color:inherit;cursor:pointer;padding:0;text-align:left}.nc-mcard-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.nc-mcard-text b{font-size:14px;color:#d8ffe8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.nc-mcard-text small{font-size:12px;opacity:.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dot.warn{background:#e0b020;box-shadow:0 0 6px #e0b02088}.nc-group-title.nc-node-title{display:flex;align-items:center;gap:6px}.nc-group-title.nc-node-title .dot{flex:0 0 auto;width:8px;height:8px;border-radius:50%;background:#355}.nc-group-title.nc-node-title .dot.on{background:#0c6;box-shadow:0 0 6px #0c68}.nc-mcard .dot{flex:0 0 auto;width:9px;height:9px;border-radius:50%;background:#355}.nc-mcard .dot.on{background:#0c6;box-shadow:0 0 6px #0c68}.nc-rel{flex:0 0 auto;font-size:11px;opacity:.45}.nc-power,.nc-menu{flex:0 0 auto;background:none;border:1px solid #243;color:#9fd;border-radius:8px;min-width:38px;min-height:38px;font-size:17px;cursor:pointer}.nc-power:active,.nc-menu:active{border-color:#2e7d32}.nc-fab{position:fixed;right:18px;bottom:18px;z-index:20;width:54px;height:54px;border-radius:50%;background:#2e7d32;color:#fff;border:none;font-size:28px;line-height:1;cursor:pointer;box-shadow:0 6px 18px #00000073}.nc-fab:active{background:#255028}.nc-lang{display:inline-flex;align-items:center;gap:2px}.nc-act{width:34px;height:34px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:#ffffff0d;border:1px solid rgba(255,255,255,.22);color:#9aa89a;cursor:pointer;flex:0 0 auto;font-size:15px;line-height:1}.nc-act.power.on{color:#0c6;border-color:#00cc668c;background:#00cc661a}.nc-act.power.warn{color:#e8c547;border-color:#e8c5478c;background:#e8c5471a}.nc-act.pin.on{color:#e8c547;border-color:#e8c5478c;background:#e8c54714}.nc-mcard{gap:8px}/**
|
|
2
2
|
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
|
3
3
|
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
|
4
4
|
* https://github.com/chjj/term.js
|
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-CEFh0hI4.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-kX5IUxt4.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="root"></div>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.8.
|
|
1
|
+
{"version":"0.8.6"}
|
package/lib/cli/fleet-service.js
CHANGED
|
@@ -185,8 +185,13 @@ function selectProviderModeSync(cfg = {}) {
|
|
|
185
185
|
|
|
186
186
|
// external: binario fidato (regular file, exec, non world-writable, no symlink).
|
|
187
187
|
// lazy-require: niente costo/accoppiamento all'import del modulo (usato solo dal companion).
|
|
188
|
-
const { binTrusted } = require('../fleet/index.js'); // sync (audit F3)
|
|
189
|
-
|
|
188
|
+
const { binTrusted, externalFleetCandidates } = require('../fleet/index.js'); // sync (audit F3)
|
|
189
|
+
// Deve usare lo stesso insieme di path del resolver runtime asincrono. In
|
|
190
|
+
// particolare Termux installa normalmente `fleet` in $PREFIX/bin: se init
|
|
191
|
+
// guardasse soltanto cfg.fleetBin potrebbe installare il companion builtin e
|
|
192
|
+
// il runtime scegliere poi l'external, creando due proprietari del boot.
|
|
193
|
+
const trustedExternal = externalFleetCandidates(cfg).find((candidate) => binTrusted(candidate)) || null;
|
|
194
|
+
const extTrusted = !!trustedExternal;
|
|
190
195
|
|
|
191
196
|
// builtin: fleet.json valido (loadDefinitions e' sync; garbage -> null = fail-closed).
|
|
192
197
|
const { loadDefinitions } = require('../fleet/definitions.js');
|
|
@@ -209,7 +214,7 @@ function selectProviderModeSync(cfg = {}) {
|
|
|
209
214
|
if (forced) return { mode: 'disabled', reason: `fail-closed: provider forzato "${forced}" non riconosciuto` };
|
|
210
215
|
|
|
211
216
|
// auto: external fidato vince (no-doppio-boot), poi builtin, poi disabled.
|
|
212
|
-
if (extTrusted) return { mode: 'external', reason:
|
|
217
|
+
if (extTrusted) return { mode: 'external', reason: `auto: external fidato configurato (${trustedExternal}; skip companion)` };
|
|
213
218
|
if (builtinAvail) return { mode: 'builtin', reason: 'auto: builtin (fleet.json valido)' };
|
|
214
219
|
return { mode: 'disabled', reason: 'auto: nessun provider disponibile (no external fidato, no fleet.json)' };
|
|
215
220
|
}
|
package/lib/fleet/builtin.js
CHANGED
|
@@ -26,7 +26,7 @@ const path = require('node:path');
|
|
|
26
26
|
const { execFile } = require('node:child_process');
|
|
27
27
|
const {
|
|
28
28
|
parseDefinitions, validateCommandTrust, resolveCwd,
|
|
29
|
-
loadDefinitions, atomicWrite, CAPS,
|
|
29
|
+
loadDefinitions, atomicWrite, CAPS, validTmuxName,
|
|
30
30
|
} = require('./definitions.js');
|
|
31
31
|
const {
|
|
32
32
|
publicCatalog, describeManaged, resolveManagedEngine, discoverOllamaModels, discoverPiModels,
|
|
@@ -193,24 +193,36 @@ function draftFrom(defs) {
|
|
|
193
193
|
...(e.env ? { env: { ...e.env } } : {}),
|
|
194
194
|
...(e.model ? { model: { ...e.model } } : {}),
|
|
195
195
|
})),
|
|
196
|
-
cells: defs.cells.map((c) => ({
|
|
196
|
+
cells: defs.cells.map((c) => ({
|
|
197
|
+
...c,
|
|
198
|
+
...(c.models ? { models: { ...c.models } } : {}),
|
|
199
|
+
...(c.permissionPolicies ? { permissionPolicies: { ...c.permissionPolicies } } : {}),
|
|
200
|
+
})),
|
|
197
201
|
};
|
|
198
202
|
}
|
|
199
203
|
|
|
200
|
-
// Applica engine + modello come un'unica transizione. Ogni engine ricorda
|
|
201
|
-
// proprio ultimo modello; passando a un altro engine
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
204
|
+
// Applica engine + modello + policy come un'unica transizione. Ogni engine ricorda
|
|
205
|
+
// il proprio ultimo modello E l'ultima policy; passando a un altro engine né
|
|
206
|
+
// l'uno né l'altra attraversano il confine. La policy e' PER-CELL PER-ENGINE:
|
|
207
|
+
// mai si tocca engine.managed.permissionPolicy (globale su tutte le celle che
|
|
208
|
+
// usano quell'engine). Valori ammessi solo 'standard' | 'unsafe'.
|
|
209
|
+
function applyCellTransition(target, engineId, { model, hasModel, policy, hasPolicy } = {}) {
|
|
210
|
+
const rememberedModels = { ...(target.models || {}) };
|
|
211
|
+
const rememberedPolicies = { ...(target.permissionPolicies || {}) };
|
|
205
212
|
target.engine = engineId;
|
|
206
213
|
if (hasModel) {
|
|
207
|
-
if (typeof model === 'string' && model)
|
|
208
|
-
else delete
|
|
214
|
+
if (typeof model === 'string' && model) rememberedModels[engineId] = model;
|
|
215
|
+
else delete rememberedModels[engineId];
|
|
216
|
+
}
|
|
217
|
+
if (hasPolicy) {
|
|
218
|
+
if (policy === 'standard' || policy === 'unsafe') rememberedPolicies[engineId] = policy;
|
|
219
|
+
else delete rememberedPolicies[engineId];
|
|
209
220
|
}
|
|
221
|
+
if (Object.keys(rememberedModels).length) target.models = rememberedModels; else delete target.models;
|
|
222
|
+
if (Object.keys(rememberedPolicies).length) target.permissionPolicies = rememberedPolicies; else delete target.permissionPolicies;
|
|
210
223
|
const selected = hasModel
|
|
211
224
|
? (typeof model === 'string' ? model : '')
|
|
212
|
-
: (
|
|
213
|
-
if (Object.keys(remembered).length) target.models = remembered; else delete target.models;
|
|
225
|
+
: (rememberedModels[engineId] || '');
|
|
214
226
|
if (selected) target.model = selected; else delete target.model;
|
|
215
227
|
}
|
|
216
228
|
|
|
@@ -264,9 +276,21 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
264
276
|
const sessions = cache.sessions;
|
|
265
277
|
const cells = cache.defs.cells.map((c) => {
|
|
266
278
|
const alive = sessions.has(c.tmuxSession);
|
|
279
|
+
// Effective policy per cella: override remembered per-engine, altrimenti il
|
|
280
|
+
// default dell'engine gestito. Esposta SOLO come policy effettiva (mai segreti).
|
|
281
|
+
const engineDef = findEngine(cache.defs, c.engine);
|
|
282
|
+
const engineDefault = engineDef && engineDef.managed ? engineDef.managed.permissionPolicy : '';
|
|
283
|
+
const remembered = c.permissionPolicies
|
|
284
|
+
&& (c.permissionPolicies[c.engine] === 'standard' || c.permissionPolicies[c.engine] === 'unsafe')
|
|
285
|
+
? c.permissionPolicies[c.engine] : null;
|
|
286
|
+
const effectivePolicy = engineDef?.managed?.client === 'pi'
|
|
287
|
+
? 'standard'
|
|
288
|
+
: (remembered || engineDefault || '');
|
|
267
289
|
return {
|
|
268
290
|
cell: c.id, tmuxSession: c.tmuxSession, engine: c.engine,
|
|
269
291
|
model: c.model || '', models: { ...(c.models || {}) },
|
|
292
|
+
permissionPolicy: effectivePolicy,
|
|
293
|
+
permissionPolicies: { ...(c.permissionPolicies || {}) },
|
|
270
294
|
active: alive, boot: c.boot, tmux: alive,
|
|
271
295
|
rc: '', key: '', degraded: false, // builtin: cella "up" <=> sessione tmux viva
|
|
272
296
|
};
|
|
@@ -391,8 +415,20 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
391
415
|
if (readonly()) throw httpError(403, 'READONLY: engine bloccato');
|
|
392
416
|
const defs = reloadDefs();
|
|
393
417
|
if (!findCell(defs, cellId)) throw httpError(400, `cella sconosciuta: ${cellId}`);
|
|
394
|
-
|
|
395
|
-
|
|
418
|
+
const selectedEngine = findEngine(defs, engId);
|
|
419
|
+
if (!selectedEngine) throw httpError(400, `engine non valido: ${engId}`);
|
|
420
|
+
const hasModel = typeof opts.model === 'string';
|
|
421
|
+
const hasPolicy = Object.prototype.hasOwnProperty.call(opts, 'permissionPolicy') && typeof opts.permissionPolicy === 'string';
|
|
422
|
+
if (hasPolicy && opts.permissionPolicy !== 'standard' && opts.permissionPolicy !== 'unsafe') {
|
|
423
|
+
throw httpError(400, 'permissionPolicy non valida (standard|unsafe)');
|
|
424
|
+
}
|
|
425
|
+
if (hasPolicy && selectedEngine.managed?.client === 'pi' && opts.permissionPolicy !== 'standard') {
|
|
426
|
+
throw httpError(400, 'Pi supporta solo permissionPolicy standard');
|
|
427
|
+
}
|
|
428
|
+
await mutate(defs, (d) => applyCellTransition(findCell(d, cellId), engId, {
|
|
429
|
+
model: opts.model, hasModel,
|
|
430
|
+
policy: hasPolicy ? opts.permissionPolicy : null, hasPolicy,
|
|
431
|
+
}));
|
|
396
432
|
const saved = reloadDefs(); const target = findCell(saved, cellId);
|
|
397
433
|
return { ok: true, engine: engId, model: target.model || '' };
|
|
398
434
|
}
|
|
@@ -450,9 +486,14 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
450
486
|
await mutate(defs, (d) => {
|
|
451
487
|
d.engines = d.engines.filter((e) => e.id !== id);
|
|
452
488
|
for (const cell of d.cells) {
|
|
453
|
-
if (
|
|
454
|
-
|
|
455
|
-
|
|
489
|
+
if (cell.models && Object.prototype.hasOwnProperty.call(cell.models, id)) {
|
|
490
|
+
delete cell.models[id];
|
|
491
|
+
if (!Object.keys(cell.models).length) delete cell.models;
|
|
492
|
+
}
|
|
493
|
+
if (cell.permissionPolicies && Object.prototype.hasOwnProperty.call(cell.permissionPolicies, id)) {
|
|
494
|
+
delete cell.permissionPolicies[id];
|
|
495
|
+
if (!Object.keys(cell.permissionPolicies).length) delete cell.permissionPolicies;
|
|
496
|
+
}
|
|
456
497
|
}
|
|
457
498
|
});
|
|
458
499
|
return { ok: true };
|
|
@@ -469,19 +510,33 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
469
510
|
if (readonly()) throw httpError(403, 'READONLY: edit-cell bloccato');
|
|
470
511
|
if (!id) throw httpError(400, 'id cell mancante');
|
|
471
512
|
const defs = reloadDefs();
|
|
472
|
-
|
|
513
|
+
const current = findCell(defs, id);
|
|
514
|
+
if (!current) throw httpError(400, `cell inesistente: ${id}`);
|
|
473
515
|
if (patch && (Object.prototype.hasOwnProperty.call(patch, 'id') || Object.prototype.hasOwnProperty.call(patch, 'tmuxSession'))) {
|
|
474
516
|
throw httpError(400, 'id e tmuxSession sono immutabili');
|
|
475
517
|
}
|
|
518
|
+
const nextEngine = findEngine(defs, typeof patch?.engine === 'string' ? patch.engine : current.engine);
|
|
519
|
+
if (Object.prototype.hasOwnProperty.call(patch || {}, 'permissionPolicy')
|
|
520
|
+
&& patch.permissionPolicy === 'unsafe' && nextEngine?.managed?.client === 'pi') {
|
|
521
|
+
throw httpError(400, 'Pi supporta solo permissionPolicy standard');
|
|
522
|
+
}
|
|
476
523
|
await mutate(defs, (d) => {
|
|
477
524
|
const target = findCell(d, id);
|
|
478
525
|
const hasEngine = typeof patch?.engine === 'string';
|
|
479
526
|
const hasModel = Object.prototype.hasOwnProperty.call(patch || {}, 'model');
|
|
527
|
+
const hasPolicy = Object.prototype.hasOwnProperty.call(patch || {}, 'permissionPolicy');
|
|
528
|
+
if (hasPolicy && patch.permissionPolicy !== 'standard' && patch.permissionPolicy !== 'unsafe' && patch.permissionPolicy !== null) {
|
|
529
|
+
throw httpError(400, 'permissionPolicy non valida (standard|unsafe)');
|
|
530
|
+
}
|
|
480
531
|
for (const [key, value] of Object.entries(patch || {})) {
|
|
481
|
-
if (key === 'engine' || key === 'model') continue;
|
|
532
|
+
if (key === 'engine' || key === 'model' || key === 'permissionPolicy') continue;
|
|
482
533
|
if (value === null) delete target[key]; else target[key] = value;
|
|
483
534
|
}
|
|
484
|
-
if (hasEngine || hasModel)
|
|
535
|
+
if (hasEngine || hasModel || hasPolicy) applyCellTransition(target, hasEngine ? patch.engine : target.engine, {
|
|
536
|
+
model: patch?.model, hasModel,
|
|
537
|
+
policy: hasPolicy ? patch.permissionPolicy : null,
|
|
538
|
+
hasPolicy,
|
|
539
|
+
});
|
|
485
540
|
});
|
|
486
541
|
const sessions = await refreshSessions();
|
|
487
542
|
return { ok: true, active: sessions.has(findCell(defs, id).tmuxSession) };
|
|
@@ -501,6 +556,63 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
501
556
|
return { ok: true };
|
|
502
557
|
}
|
|
503
558
|
|
|
559
|
+
// importCell: riconcilia una sessione tmux esistente (es. cella Fleet legacy
|
|
560
|
+
// rimasta orfana su installazione pulita, tipo "jarvis") in una cella GESTITA
|
|
561
|
+
// di fleet.json. Design §reconciliation:
|
|
562
|
+
// - idempotente: se una cella gestita possiede gia' questa tmuxSession -> no-op.
|
|
563
|
+
// - NESSUNA invenzione di engine/provider/model: l'engine DEV'essere gia'
|
|
564
|
+
// dichiarato in fleet.json (l'operatore lo sceglie). model/policy restano i
|
|
565
|
+
// default dell'engine scelto (modificabili dopo dal pannello di lancio).
|
|
566
|
+
// - tmuxSession non canonica (es. "jarvis") è ammessa e round-trip; una
|
|
567
|
+
// sessione legacy canonica cloud-X viene adottata come cella X.
|
|
568
|
+
// - cwd obbligatoria (default = home); la validazione strict (parseCell, sotto
|
|
569
|
+
// home, no cloud-* alias incoerenti) avviene in atomicWrite -> errore chiaro.
|
|
570
|
+
// Dopo l'import la sessione sparisce da "unmanaged" e compare una sola volta in
|
|
571
|
+
// Fleet con gear+power. Elimina/terminate resta solo nelle Impostazioni.
|
|
572
|
+
const CELL_ID_RE = /^[A-Za-z0-9._-]{1,32}$/;
|
|
573
|
+
function deriveCellId(session, cells) {
|
|
574
|
+
const raw = String(session || '');
|
|
575
|
+
// Una sessione legacy canonica cloud-X si adotta come cella X. Usare
|
|
576
|
+
// direttamente cloud-X come id produrrebbe cloud-cloud-X e fallirebbe la
|
|
577
|
+
// validazione; un id esplicito diverso continua invece a essere rifiutato.
|
|
578
|
+
const canonicalSuffix = raw.startsWith('cloud-') ? raw.slice('cloud-'.length) : '';
|
|
579
|
+
let base = (canonicalSuffix && CELL_ID_RE.test(canonicalSuffix) ? canonicalSuffix : raw)
|
|
580
|
+
.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 32) || 'cell';
|
|
581
|
+
const used = new Set(cells.map((c) => c.id));
|
|
582
|
+
if (!used.has(base)) return base;
|
|
583
|
+
if (canonicalSuffix) return base; // il caller produrrà un 409 esplicito
|
|
584
|
+
for (let i = 2; i < 100; i += 1) {
|
|
585
|
+
const candidate = `${base.slice(0, Math.max(1, 32 - String(i).length - 1))}-${i}`;
|
|
586
|
+
if (!used.has(candidate)) return candidate;
|
|
587
|
+
}
|
|
588
|
+
return base;
|
|
589
|
+
}
|
|
590
|
+
async function importCell(b = {}) {
|
|
591
|
+
if (readonly()) throw httpError(403, 'READONLY: import bloccato');
|
|
592
|
+
const tmuxSession = typeof b.tmuxSession === 'string' ? b.tmuxSession.trim() : '';
|
|
593
|
+
if (!tmuxSession || !validTmuxName(tmuxSession)) throw httpError(400, 'tmuxSession non valida');
|
|
594
|
+
const defs = reloadDefs();
|
|
595
|
+
const existing = defs.cells.find((c) => c.tmuxSession === tmuxSession);
|
|
596
|
+
if (existing) return { ok: true, id: existing.id, tmuxSession, idempotent: true };
|
|
597
|
+
const sessions = await refreshSessions();
|
|
598
|
+
if (!sessions.has(tmuxSession)) {
|
|
599
|
+
throw httpError(404, `sessione tmux non trovata: ${tmuxSession}`);
|
|
600
|
+
}
|
|
601
|
+
const engineId = typeof b.engine === 'string' ? b.engine.trim() : '';
|
|
602
|
+
if (!engineId || !findEngine(defs, engineId)) {
|
|
603
|
+
throw httpError(400, `engine non dichiarato: "${engineId || '(manca)'}" — crea prima l'engine in Impostazioni`);
|
|
604
|
+
}
|
|
605
|
+
let id = typeof b.id === 'string' && b.id.trim() ? b.id.trim() : deriveCellId(tmuxSession, defs.cells);
|
|
606
|
+
if (!CELL_ID_RE.test(id)) throw httpError(400, 'id non valido (A-Za-z0-9._- max 32)');
|
|
607
|
+
if (defs.cells.some((c) => c.id === id)) throw httpError(409, `id cella già usato: ${id}`);
|
|
608
|
+
const cwd = typeof b.cwd === 'string' && b.cwd.trim() ? b.cwd.trim() : home;
|
|
609
|
+
const cellDef = { id, cwd, engine: engineId, boot: b.boot === true, tmuxSession };
|
|
610
|
+
try {
|
|
611
|
+
await mutate(defs, (d) => { d.cells.push(cellDef); });
|
|
612
|
+
} catch (e) { throw httpError(400, `import non valido: ${e.message}`); }
|
|
613
|
+
return { ok: true, id, tmuxSession, imported: true };
|
|
614
|
+
}
|
|
615
|
+
|
|
504
616
|
// Vista editabile ma secret-safe: gli env values restano write-only.
|
|
505
617
|
function definitions() {
|
|
506
618
|
const defs = reloadDefs();
|
|
@@ -512,7 +624,11 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
512
624
|
if (e.managed) out.managedInfo = describeManaged(e.managed, { ...cfg, home });
|
|
513
625
|
return out;
|
|
514
626
|
}),
|
|
515
|
-
cells: defs.cells.map((c) => ({
|
|
627
|
+
cells: defs.cells.map((c) => ({
|
|
628
|
+
...c,
|
|
629
|
+
...(c.models ? { models: { ...c.models } } : {}),
|
|
630
|
+
...(c.permissionPolicies ? { permissionPolicies: { ...c.permissionPolicies } } : {}),
|
|
631
|
+
})),
|
|
516
632
|
managedCatalog: publicCatalog(),
|
|
517
633
|
managedConfig: { providerSecretsPath: cfg.providerSecretsPath || path.join(home, '.nexuscrew', 'providers.env') },
|
|
518
634
|
};
|
|
@@ -581,13 +697,14 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
581
697
|
boot: { type: 'boolean', required: false, default: false },
|
|
582
698
|
model: { type: 'string', required: false, max: CAPS.MAX_MODEL_VAL_LEN },
|
|
583
699
|
models: { type: 'object', required: false, keyRef: 'engine.id', valueMax: CAPS.MAX_MODEL_VAL_LEN },
|
|
700
|
+
permissionPolicies: { type: 'object', required: false, keyRef: 'engine.id', valueEnum: ['standard', 'unsafe'] },
|
|
584
701
|
prompt: { type: 'string', required: false, max: CAPS.MAX_PROMPT_LEN },
|
|
585
702
|
},
|
|
586
703
|
};
|
|
587
704
|
}
|
|
588
705
|
|
|
589
706
|
function capabilities() {
|
|
590
|
-
return ['status', 'up', 'down', 'restart', 'engine', 'boot', 'define', 'edit', 'remove', 'schema', 'definitions'];
|
|
707
|
+
return ['status', 'up', 'down', 'restart', 'engine', 'boot', 'define', 'edit', 'remove', 'import', 'schema', 'definitions'];
|
|
591
708
|
}
|
|
592
709
|
|
|
593
710
|
return {
|
|
@@ -595,7 +712,7 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
595
712
|
provider: 'builtin',
|
|
596
713
|
status, up, down, restart, engine: setEngine, boot: setBoot, isCellSession,
|
|
597
714
|
defineEngine, editEngine, removeEngine,
|
|
598
|
-
defineCell, editCell, removeCell,
|
|
715
|
+
defineCell, editCell, removeCell, importCell,
|
|
599
716
|
schema, definitions, capabilities,
|
|
600
717
|
};
|
|
601
718
|
}
|
package/lib/fleet/definitions.js
CHANGED
|
@@ -254,6 +254,24 @@ function parseCell(c, engineIds) {
|
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
// permissionPolicies: scelta PER-CELL PER-ENGINE (override del default engine).
|
|
258
|
+
// Stesso confine di `models`: ricorda l'ultima policy usata con ogni engine, così
|
|
259
|
+
// tornando a un provider si ritrova la scelta precedente senza trascinarla altrove
|
|
260
|
+
// e SENZA toccare engine.managed.permissionPolicy (globale: cambierebbe ogni cella
|
|
261
|
+
// che usa quell'engine). Valori ammessi solo 'standard' | 'unsafe'.
|
|
262
|
+
let permissionPolicies;
|
|
263
|
+
if (c.permissionPolicies !== undefined) {
|
|
264
|
+
if (!c.permissionPolicies || typeof c.permissionPolicies !== 'object' || Array.isArray(c.permissionPolicies)) return null;
|
|
265
|
+
const entries = Object.entries(c.permissionPolicies);
|
|
266
|
+
if (entries.length > MAX_ENGINES) return null;
|
|
267
|
+
permissionPolicies = {};
|
|
268
|
+
for (const [engineId, value] of entries) {
|
|
269
|
+
if (!engineIds.has(engineId)) return null;
|
|
270
|
+
if (value !== 'standard' && value !== 'unsafe') return null;
|
|
271
|
+
permissionPolicies[engineId] = value;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
257
275
|
// prompt (opzionale, cap)
|
|
258
276
|
let prompt;
|
|
259
277
|
if (c.prompt !== undefined) {
|
|
@@ -280,6 +298,7 @@ function parseCell(c, engineIds) {
|
|
|
280
298
|
const out = { id: c.id, cwd: c.cwd, engine: c.engine, boot, tmuxSession };
|
|
281
299
|
if (model !== undefined) out.model = model;
|
|
282
300
|
if (Object.keys(models).length) out.models = models;
|
|
301
|
+
if (permissionPolicies) out.permissionPolicies = permissionPolicies;
|
|
283
302
|
if (prompt !== undefined) out.prompt = prompt;
|
|
284
303
|
return out;
|
|
285
304
|
}
|
|
@@ -403,6 +422,7 @@ module.exports = {
|
|
|
403
422
|
resolveCwd,
|
|
404
423
|
loadDefinitions,
|
|
405
424
|
atomicWrite,
|
|
425
|
+
validTmuxName,
|
|
406
426
|
CAPS,
|
|
407
427
|
// Costanti esposte anche piatte (comode per la UI/schema e i test)
|
|
408
428
|
SCHEMA_VERSION, MAX_ENGINES, MAX_CELLS, MAX_ARGS, MAX_ARG_LEN,
|
package/lib/fleet/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const fs = require('node:fs');
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const path = require('node:path');
|
|
3
5
|
const { createFleetExec } = require('./exec.js');
|
|
4
6
|
|
|
5
7
|
const STATUS_TTL_MS = 2000;
|
|
@@ -135,4 +137,43 @@ async function createFleet(cfg = {}) {
|
|
|
135
137
|
};
|
|
136
138
|
}
|
|
137
139
|
|
|
138
|
-
|
|
140
|
+
// Discovery del binario fleet legacy cross-platform. In precedenza si cercava
|
|
141
|
+
// solo cfg.fleetBin (~/.local/bin/fleet di default); su Termux un fleet può stare
|
|
142
|
+
// in $PREFIX/bin/fleet. Ordine candidati: (1) cfg.fleetBin (config/env esplicita),
|
|
143
|
+
// (2) $PREFIX/bin/fleet (Termux), (3) ~/.local/bin/fleet (default storico).
|
|
144
|
+
// Dedup + skip vuoti. La validità (regular/trusted + status --json schema valido)
|
|
145
|
+
// la verifica il chiamante (resolveExternalFleet) via binTrusted + createFleet.
|
|
146
|
+
function fleetCandidates(cfg = {}) {
|
|
147
|
+
const home = cfg.home || os.homedir();
|
|
148
|
+
const envPrefix = (cfg.env && cfg.env.PREFIX) || process.env.PREFIX;
|
|
149
|
+
const out = [];
|
|
150
|
+
const push = (p) => { if (typeof p === 'string' && p && !out.includes(p)) out.push(p); };
|
|
151
|
+
push(cfg.fleetBin);
|
|
152
|
+
if (envPrefix) push(path.join(envPrefix, 'bin', 'fleet'));
|
|
153
|
+
push(path.join(home, '.local', 'bin', 'fleet'));
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// In auto mode i fallback cross-platform sono intenzionali. In forced external,
|
|
158
|
+
// invece, un fleetBin fornito è un pin: se non è valido si fallisce chiuso e non
|
|
159
|
+
// si cambia eseguibile alle spalle dell'operatore.
|
|
160
|
+
function externalFleetCandidates(cfg = {}) {
|
|
161
|
+
const forced = (cfg.fleetProvider || process.env.NEXUSCREW_FLEET_PROVIDER || '').toLowerCase();
|
|
162
|
+
if (forced === 'external' && typeof cfg.fleetBin === 'string' && cfg.fleetBin) return [cfg.fleetBin];
|
|
163
|
+
return fleetCandidates(cfg);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Prova i candidati in ordine e ritorna il primo external FIDATO (binTrusted) E
|
|
167
|
+
// rispondente al contratto (createFleet -> available, che fa status --json e
|
|
168
|
+
// valida lo schema ai-fleet). Null se nessuno valido. Così un external reale
|
|
169
|
+
// resta external (non cade sul builtin e non doppia il boot).
|
|
170
|
+
async function resolveExternalFleet(cfg = {}) {
|
|
171
|
+
for (const bin of externalFleetCandidates(cfg)) {
|
|
172
|
+
if (!binTrusted(bin)) continue;
|
|
173
|
+
const f = await createFleet({ ...cfg, fleetBin: bin });
|
|
174
|
+
if (f.available) return { fleet: f, bin, reason: `fidato + risponde al contratto (${bin})` };
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = { createFleet, parseStatus, binTrusted, fleetCandidates, externalFleetCandidates, resolveExternalFleet };
|
package/lib/fleet/managed.js
CHANGED
|
@@ -333,7 +333,18 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
333
333
|
const profile = profileFor(spec.client, spec.provider, spec.credentialProfile || '');
|
|
334
334
|
const cred = credential(profile, spec, cfg, home);
|
|
335
335
|
const env = {}; const args = []; const model = cell?.model || spec.model;
|
|
336
|
-
|
|
336
|
+
// Effective permission policy: override PER-CELL PER-ENGINE (remembered) vince sul
|
|
337
|
+
// default dell'engine. Mai si mutationa engine.managed.permissionPolicy (globale).
|
|
338
|
+
// Pi resta sempre 'standard' (lo spec normalized rifiuta gia' unsafe per pi).
|
|
339
|
+
const engineId = engine && typeof engine.id === 'string' ? engine.id : '';
|
|
340
|
+
const override = cell && cell.permissionPolicies && Object.prototype.hasOwnProperty.call(cell.permissionPolicies, engineId)
|
|
341
|
+
? cell.permissionPolicies[engineId] : null;
|
|
342
|
+
let effectivePolicy = (override === 'standard' || override === 'unsafe') ? override : spec.permissionPolicy;
|
|
343
|
+
// Pi resta sempre 'standard': normalizeManagedSpec rifiuta gia' unsafe nello spec
|
|
344
|
+
// dell'engine, ma l'override PER-CELL bypasserebbe quel check -> clamp esplicito.
|
|
345
|
+
if (spec.client === 'pi') effectivePolicy = 'standard';
|
|
346
|
+
info.permissionPolicy = effectivePolicy;
|
|
347
|
+
if (effectivePolicy === 'unsafe') {
|
|
337
348
|
if (spec.client === 'claude') args.push('--dangerously-skip-permissions');
|
|
338
349
|
if (spec.client === 'codex' || spec.client === 'codex-vl') args.push('--dangerously-bypass-approvals-and-sandbox');
|
|
339
350
|
}
|
|
@@ -345,12 +356,14 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
345
356
|
} else {
|
|
346
357
|
const endpoint = spec.baseUrl || profile.endpoint;
|
|
347
358
|
const token = profile.auth === 'none' ? 'ollama' : cred.value;
|
|
359
|
+
const contextWindow = OLLAMA_CONTEXT[model] || (spec.provider === 'zai' ? 1000000 : 200000);
|
|
348
360
|
Object.assign(env, {
|
|
349
361
|
ANTHROPIC_BASE_URL: endpoint, ANTHROPIC_AUTH_TOKEN: token, ANTHROPIC_API_KEY: '',
|
|
350
362
|
ANTHROPIC_MODEL: model, ANTHROPIC_DEFAULT_OPUS_MODEL: model,
|
|
351
363
|
ANTHROPIC_DEFAULT_SONNET_MODEL: model, ANTHROPIC_DEFAULT_HAIKU_MODEL: model,
|
|
352
364
|
CLAUDE_CODE_SUBAGENT_MODEL: model, API_TIMEOUT_MS: '3000000',
|
|
353
|
-
|
|
365
|
+
CLAUDE_CODE_MAX_CONTEXT_TOKENS: String(contextWindow),
|
|
366
|
+
CLAUDE_CODE_AUTO_COMPACT_WINDOW: String(contextWindow),
|
|
354
367
|
});
|
|
355
368
|
}
|
|
356
369
|
if (model) args.push('--model', model);
|
package/lib/fleet/provider.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// Il drift a runtime NON si risolve qui: e' una scelta one-shot. Se l'external
|
|
15
15
|
// diventa invalido DOPO lo startup, lo status risultante sara' degraded/unavailable
|
|
16
16
|
// (mai fall-through silenzioso al built-in) — gestito dal layer di status.
|
|
17
|
-
const {
|
|
17
|
+
const { binTrusted, externalFleetCandidates, resolveExternalFleet } = require('./index.js');
|
|
18
18
|
const { createBuiltinFleet } = require('./builtin.js');
|
|
19
19
|
|
|
20
20
|
const DISABLED_FLEET = Object.freeze({
|
|
@@ -37,19 +37,20 @@ async function selectProvider(cfg = {}) {
|
|
|
37
37
|
|
|
38
38
|
const forced = (cfg.fleetProvider || process.env.NEXUSCREW_FLEET_PROVIDER || '').toLowerCase() || null;
|
|
39
39
|
|
|
40
|
-
// External:
|
|
40
|
+
// External: scopre il binario fleet legacy cross-platform (cfg.fleetBin,
|
|
41
|
+
// $PREFIX/bin/fleet su Termux, ~/.local/bin/fleet) e accetta il primo fidato
|
|
42
|
+
// (binTrusted) che risponde al contratto (status --json con schema valido).
|
|
41
43
|
let extFleet = null;
|
|
42
44
|
let extReason = null;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
else extReason = 'binario fidato ma non risponde al contratto (status invalido)';
|
|
48
|
-
} else {
|
|
49
|
-
extReason = 'binario non fidato (symlink/world-writable/non exec)';
|
|
50
|
-
}
|
|
45
|
+
const resolvedExternal = await resolveExternalFleet(cfg);
|
|
46
|
+
if (resolvedExternal) {
|
|
47
|
+
extFleet = resolvedExternal.fleet;
|
|
48
|
+
extReason = resolvedExternal.reason;
|
|
51
49
|
} else {
|
|
52
|
-
|
|
50
|
+
// Diagnosi: perché nessun candidato external è valido (fidato + contratto).
|
|
51
|
+
const cands = externalFleetCandidates(cfg);
|
|
52
|
+
if (!cands.length) extReason = 'nessun fleetBin esterno configurato';
|
|
53
|
+
else extReason = `nessun candidato external valido: ${cands.map((b) => (binTrusted(b) ? `${b} (non risponde al contratto)` : `${b} (non fidato)`)).join('; ')}`;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
// Builtin: abilitato + fleet.json valido.
|
package/lib/fleet/routes.js
CHANGED
|
@@ -78,7 +78,9 @@ function fleetRoutes(fleetP, cfg = {}) {
|
|
|
78
78
|
r.post('/up', guard(async (f, b) => {
|
|
79
79
|
const cell = String(b.cell || '');
|
|
80
80
|
if (b.engine && capList(f).includes('edit')) {
|
|
81
|
-
|
|
81
|
+
const opts = { model: typeof b.model === 'string' ? b.model : '' };
|
|
82
|
+
if (typeof b.permissionPolicy === 'string') opts.permissionPolicy = b.permissionPolicy;
|
|
83
|
+
await f.engine(cell, String(b.engine), opts);
|
|
82
84
|
}
|
|
83
85
|
if (Object.prototype.hasOwnProperty.call(b, 'boot') && capList(f).includes('edit') && capList(f).includes('boot')) {
|
|
84
86
|
await f.boot(cell, b.boot === true);
|
|
@@ -91,7 +93,11 @@ function fleetRoutes(fleetP, cfg = {}) {
|
|
|
91
93
|
return f.down(cell, { boot: !!b.boot });
|
|
92
94
|
}, { mutate: true }));
|
|
93
95
|
r.post('/restart', guard((f, b) => { requireCap(f, 'restart'); return f.restart(String(b.cell || '')); }, { mutate: true }));
|
|
94
|
-
r.post('/engine', guard((f, b) =>
|
|
96
|
+
r.post('/engine', guard((f, b) => {
|
|
97
|
+
const opts = { model: typeof b.model === 'string' ? b.model : '' };
|
|
98
|
+
if (typeof b.permissionPolicy === 'string') opts.permissionPolicy = b.permissionPolicy;
|
|
99
|
+
return f.engine(String(b.cell || ''), String(b.engine || ''), opts);
|
|
100
|
+
}, { mutate: true }));
|
|
95
101
|
r.post('/boot', guard((f, b) => f.boot(String(b.cell || ''), b.enabled === true), { mutate: true }));
|
|
96
102
|
|
|
97
103
|
// --- Estensione B4.2: schema + define/edit/remove (engine e cell) ---
|
|
@@ -104,6 +110,9 @@ function fleetRoutes(fleetP, cfg = {}) {
|
|
|
104
110
|
r.post('/define-cell', guard((f, b) => { requireCap(f, 'define'); return f.defineCell(b.def); }, { mutate: true }));
|
|
105
111
|
r.post('/edit-cell', guard((f, b) => { requireCap(f, 'edit'); return f.editCell(b.id, b.patch); }, { mutate: true }));
|
|
106
112
|
r.post('/remove-cell', guard((f, b) => { requireCap(f, 'remove'); return f.removeCell(b.id, { stop: b.stop === true }); }, { mutate: true }));
|
|
113
|
+
// Riconciliazione sessione tmux esistente (cella Fleet legacy orfana) in cella
|
|
114
|
+
// gestita fleet.json. Capability 'define' (solo builtin): external legacy -> 501.
|
|
115
|
+
r.post('/import-cell', guard(async (f, b) => { requireCap(f, 'import'); return f.importCell(b || {}); }, { mutate: true }));
|
|
107
116
|
|
|
108
117
|
return r;
|
|
109
118
|
}
|
package/lib/nodes/peering.js
CHANGED
|
@@ -40,13 +40,44 @@ function encodePairing(data) {
|
|
|
40
40
|
return Buffer.from(JSON.stringify(data)).toString('base64url');
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// Allowlist rigorosa per versione (caps). v1: solo {v,instanceId,port,label,invite}.
|
|
44
|
+
// v2 aggiunge name (slug suggerito), ssh (target/alias), sshPort?: routing non
|
|
45
|
+
// segreto. NESSUN campo segreto è ammesso oltre l'invite one-time: niente
|
|
46
|
+
// identityFile, chiave privata, API key, bearer UI. Unknown field -> null.
|
|
47
|
+
const PAIRING_V1_KEYS = new Set(['v', 'instanceId', 'port', 'label', 'invite']);
|
|
48
|
+
const PAIRING_V2_KEYS = new Set(['v', 'instanceId', 'port', 'label', 'invite', 'name', 'ssh', 'sshPort']);
|
|
49
|
+
|
|
43
50
|
function decodePairing(value) {
|
|
51
|
+
let x;
|
|
44
52
|
try {
|
|
45
|
-
|
|
46
|
-
if (!x || x.v !== 1 || !store.NODE_ID_RE.test(x.instanceId) || !store.isPort(x.port)
|
|
47
|
-
|| !store.validToken(x.invite) || typeof x.label !== 'string') return null;
|
|
48
|
-
return x;
|
|
53
|
+
x = JSON.parse(Buffer.from(String(value || ''), 'base64url').toString('utf8'));
|
|
49
54
|
} catch (_) { return null; }
|
|
55
|
+
if (!x || typeof x !== 'object' || Array.isArray(x)) return null;
|
|
56
|
+
const allowed = x.v === 1 ? PAIRING_V1_KEYS : x.v === 2 ? PAIRING_V2_KEYS : null;
|
|
57
|
+
if (!allowed) return null;
|
|
58
|
+
for (const k of Object.keys(x)) if (!allowed.has(k)) return null; // strict: unknown -> null
|
|
59
|
+
// core obbligatorio (entrambe le versioni)
|
|
60
|
+
if (!store.NODE_ID_RE.test(x.instanceId) || !store.isPort(x.port)
|
|
61
|
+
|| !store.validToken(x.invite) || typeof x.label !== 'string') return null;
|
|
62
|
+
const out = { v: x.v, instanceId: x.instanceId, port: x.port, label: x.label, invite: x.invite };
|
|
63
|
+
if (x.v === 2) {
|
|
64
|
+
if (x.name !== undefined) {
|
|
65
|
+
if (typeof x.name !== 'string' || !store.NODE_NAME_RE.test(x.name)) return null;
|
|
66
|
+
out.name = x.name;
|
|
67
|
+
}
|
|
68
|
+
if (x.ssh !== undefined) {
|
|
69
|
+
// target/alias SSH (user@host o Host alias); MAI secret. parseSshTarget
|
|
70
|
+
// rifiuta whitespace/control/leading '-' (argv-safe, non diventa flag ssh).
|
|
71
|
+
const ssh = typeof x.ssh === 'string' ? store.parseSshTarget(x.ssh) : null;
|
|
72
|
+
if (!ssh) return null;
|
|
73
|
+
out.ssh = ssh.value;
|
|
74
|
+
}
|
|
75
|
+
if (x.sshPort !== undefined) {
|
|
76
|
+
if (!store.isPort(x.sshPort)) return null;
|
|
77
|
+
out.sshPort = x.sshPort;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
50
81
|
}
|
|
51
82
|
|
|
52
83
|
function parsePairingUrl(value) {
|
|
@@ -57,14 +88,35 @@ function parsePairingUrl(value) {
|
|
|
57
88
|
} catch (_) { return null; }
|
|
58
89
|
}
|
|
59
90
|
|
|
60
|
-
function createInvite({
|
|
91
|
+
function createInvite({
|
|
92
|
+
invitesPath, instanceId, port, label = 'NexusCrew', now = Date.now(), randomBytes = crypto.randomBytes,
|
|
93
|
+
ssh, sshPort, name,
|
|
94
|
+
} = {}) {
|
|
61
95
|
const invite = randomBytes(32).toString('base64url');
|
|
62
96
|
const expiresAt = now + INVITE_TTL_MS;
|
|
63
97
|
const rows = readInvites(invitesPath, now);
|
|
64
98
|
rows.push({ hash: crypto.createHash('sha256').update(invite).digest('hex'), expiresAt, usedAt: null });
|
|
65
99
|
writeInvites(invitesPath, rows.slice(-32));
|
|
66
|
-
const
|
|
67
|
-
|
|
100
|
+
const payload = { v: 1, instanceId, port, label: String(label).slice(0, 64), invite };
|
|
101
|
+
// v2: include Host/alias SSH + slug quando forniti (routing non segreto). Così
|
|
102
|
+
// il ricevente incolla/scansiona UN solo link e ha tutto per precompilare il
|
|
103
|
+
// form. Senza ssh/name resta v1 (backward-compat con link 0.8.x esistenti).
|
|
104
|
+
const sshVal = typeof ssh === 'string' && ssh.trim() ? store.parseSshTarget(ssh.trim()) : null;
|
|
105
|
+
const requestedName = typeof name === 'string' && name.trim() && store.NODE_NAME_RE.test(name.trim())
|
|
106
|
+
? name.trim()
|
|
107
|
+
: '';
|
|
108
|
+
// Un link con routing SSH deve sempre portare anche lo slug. Se il chiamante
|
|
109
|
+
// non lo specifica, lo ricaviamo dalla label del dispositivo: il ricevente non
|
|
110
|
+
// deve compilare altri campi per arrivare a "testa e collega".
|
|
111
|
+
const linkName = requestedName || (sshVal ? store.toSlug(payload.label) : '');
|
|
112
|
+
if (sshVal || linkName) {
|
|
113
|
+
payload.v = 2;
|
|
114
|
+
if (linkName) payload.name = linkName;
|
|
115
|
+
if (sshVal) payload.ssh = sshVal.value;
|
|
116
|
+
if (sshVal && store.isPort(sshPort)) payload.sshPort = sshPort;
|
|
117
|
+
}
|
|
118
|
+
const pair = encodePairing(payload);
|
|
119
|
+
return { pairingUrl: `http://127.0.0.1:${port}/#pair=${pair}`, expiresAt, version: payload.v };
|
|
68
120
|
}
|
|
69
121
|
|
|
70
122
|
function consumeInvite({ invitesPath, invite, now = Date.now() }) {
|