@mmmbuto/nexuscrew 0.8.27 → 0.8.29
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 +41 -0
- package/README.md +24 -9
- package/frontend/dist/assets/{index-xxfypte7.css → index-BAzlX2nP.css} +1 -1
- package/frontend/dist/assets/index-BJdAdNOq.js +93 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/cli/commands.js +96 -3
- package/lib/cli/doctor.js +123 -11
- package/lib/cli/fleet-service.js +9 -10
- package/lib/cli/init.js +8 -1
- package/lib/cli/service.js +7 -4
- package/lib/diagnostics/store.js +2 -2
- package/lib/fleet/builtin.js +206 -17
- package/lib/fleet/causes.js +68 -0
- package/lib/fleet/definitions.js +86 -4
- package/lib/fleet/launch.js +25 -1
- package/lib/fleet/managed.js +62 -9
- package/lib/fleet/routes.js +16 -2
- package/lib/fleet/runtime.js +69 -33
- package/package.json +1 -1
- package/frontend/dist/assets/index-CWKyv6KS.js +0 -93
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' });
|
|
64
|
+
const CLIENT_LABELS = Object.freeze({ claude: 'Claude Code', codex: 'Codex', 'codex-vl': 'Codex-VL', pi: 'Pi', shell: 'Shell' });
|
|
65
65
|
const PROVIDER_ID_RE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
66
66
|
|
|
67
67
|
function validBaseUrl(value) {
|
|
@@ -109,6 +109,9 @@ const CATALOG = Object.freeze([
|
|
|
109
109
|
// Pi uses its real provider IDs directly. OAuth providers do not need env keys.
|
|
110
110
|
{ id: 'pi.native', client: 'pi', provider: 'native', label: 'Pi configured default', auth: 'login', protocol: 'pi_native', default: true, core: true },
|
|
111
111
|
{ id: 'pi.alibaba-token-plan', client: 'pi', provider: 'alibaba-token-plan', label: 'Alibaba Token Plan Personal', auth: 'ALIBABA_CODE_API_KEY', endpoint: 'https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1', protocol: 'openai-completions', model: 'qwen3.8-max-preview', models: ALIBABA_TOKEN_PLAN_MODELS, strictModels: true, piProvider: 'alibaba-token-plan', piExtension: { baseUrl: 'https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1', models: ALIBABA_PI_MODELS }, delegatePiAuth: false, core: true, notice: 'alibaba-token-plan' },
|
|
112
|
+
// Device-local shell. Kept after the established default clients so adding
|
|
113
|
+
// it does not change the preselected engine for a newly-created cell.
|
|
114
|
+
{ id: 'shell.local', client: 'shell', provider: 'local', label: 'Shell', auth: 'none', protocol: 'shell', default: true, core: true },
|
|
112
115
|
{ id: 'pi.anthropic', client: 'pi', provider: 'anthropic', label: 'Anthropic', auth: 'ANTHROPIC_API_KEY', protocol: 'pi_native', piProvider: 'anthropic', core: true },
|
|
113
116
|
{ id: 'pi.openai', client: 'pi', provider: 'openai', label: 'OpenAI API', auth: 'OPENAI_API_KEY', protocol: 'pi_native', piProvider: 'openai', core: true },
|
|
114
117
|
{ id: 'pi.openai-codex', client: 'pi', provider: 'openai-codex', label: 'OpenAI Codex OAuth', auth: 'login', protocol: 'pi_native', piProvider: 'openai-codex', core: true },
|
|
@@ -148,11 +151,12 @@ function normalizeManagedSpec(value) {
|
|
|
148
151
|
if (!profile) return null;
|
|
149
152
|
const model = value.model === undefined ? (profile.model || '') : value.model;
|
|
150
153
|
if (typeof model !== 'string' || model.length > 128 || /[\x00-\x1f\x7f]/.test(model)) return null;
|
|
154
|
+
if (value.client === 'shell' && model) return null;
|
|
151
155
|
if (profile.requiresModel && !model) return null;
|
|
152
156
|
if (profile.strictModels && !(profile.models || []).includes(model)) return null;
|
|
153
157
|
const permissionPolicy = value.permissionPolicy === undefined ? (profile.client === 'claude' ? 'unsafe' : 'standard') : value.permissionPolicy;
|
|
154
158
|
if (permissionPolicy !== 'standard' && permissionPolicy !== 'unsafe') return null;
|
|
155
|
-
if (value.client === 'pi' && permissionPolicy !== 'standard') return null;
|
|
159
|
+
if ((value.client === 'pi' || value.client === 'shell') && permissionPolicy !== 'standard') return null;
|
|
156
160
|
const out = { client: profile.client, provider: profile.provider, model, permissionPolicy };
|
|
157
161
|
if (profile.credentialProfile) out.credentialProfile = profile.credentialProfile;
|
|
158
162
|
if (profile.credentialEnv) {
|
|
@@ -188,6 +192,16 @@ function defaultDefinitions() {
|
|
|
188
192
|
};
|
|
189
193
|
}
|
|
190
194
|
|
|
195
|
+
function defaultShellEngine() {
|
|
196
|
+
const profile = CATALOG.find((entry) => entry.id === 'shell.local');
|
|
197
|
+
return {
|
|
198
|
+
id: profile.id,
|
|
199
|
+
label: CLIENT_LABELS.shell,
|
|
200
|
+
rc: false,
|
|
201
|
+
managed: { client: 'shell', provider: 'local', model: '', permissionPolicy: 'standard' },
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
191
205
|
function parseAssignments(raw) {
|
|
192
206
|
const out = {};
|
|
193
207
|
for (const line of raw.split(/\r?\n/)) {
|
|
@@ -269,6 +283,35 @@ function findBinary(client, home) {
|
|
|
269
283
|
return null;
|
|
270
284
|
}
|
|
271
285
|
|
|
286
|
+
// Resolve a device-local interactive shell without persisting a path in
|
|
287
|
+
// fleet.json. Candidates are ordered and fail closed. Symlinks are resolved
|
|
288
|
+
// first, then the existing command trust policy is applied to the real file.
|
|
289
|
+
function resolveInteractiveShell(cfg = {}) {
|
|
290
|
+
const env = cfg.env || process.env;
|
|
291
|
+
const platform = cfg.platform || process.platform;
|
|
292
|
+
const termux = termuxRuntimePaths(env, { platform, home: cfg.home });
|
|
293
|
+
const candidates = [];
|
|
294
|
+
if (typeof env.SHELL === 'string' && path.isAbsolute(env.SHELL)) candidates.push(env.SHELL);
|
|
295
|
+
if (termux?.prefix) {
|
|
296
|
+
candidates.push(path.join(termux.prefix, 'bin', 'bash'));
|
|
297
|
+
candidates.push(path.join(termux.prefix, 'bin', 'sh'));
|
|
298
|
+
}
|
|
299
|
+
candidates.push('/bin/bash', '/bin/sh');
|
|
300
|
+
const validate = cfg.validateCommandTrust
|
|
301
|
+
|| ((command) => require('./definitions.js').validateCommandTrust(command));
|
|
302
|
+
for (const candidate of [...new Set(candidates)]) {
|
|
303
|
+
try {
|
|
304
|
+
const real = fs.realpathSync(candidate);
|
|
305
|
+
if (validate(real).ok) return real;
|
|
306
|
+
} catch (_) { /* next candidate */ }
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function shellLoginArgs(command) {
|
|
312
|
+
return ['bash', 'zsh', 'sh', 'dash'].includes(path.basename(String(command || ''))) ? ['-l'] : [];
|
|
313
|
+
}
|
|
314
|
+
|
|
272
315
|
// Termux reports process.platform === 'android' and deliberately has no
|
|
273
316
|
// /usr/bin/env. npm CLI shims commonly resolve to a JavaScript file with
|
|
274
317
|
// `#!/usr/bin/env node`; direct tmux exec then fails in the kernel before the
|
|
@@ -422,7 +465,9 @@ function describeManaged(spec, cfg = {}) {
|
|
|
422
465
|
if (!normalized) return { configured: false, reason: 'invalid managed profile' };
|
|
423
466
|
const home = cfg.home || require('node:os').homedir();
|
|
424
467
|
const profile = profileFor(normalized.client, normalized.provider, normalized.credentialProfile || '');
|
|
425
|
-
const binary =
|
|
468
|
+
const binary = normalized.client === 'shell'
|
|
469
|
+
? resolveInteractiveShell({ ...cfg, home })
|
|
470
|
+
: findBinary(normalized.client, home);
|
|
426
471
|
const cred = credential(profile, normalized, cfg, home);
|
|
427
472
|
// Pi can resolve credentials from its own documented /login auth store. Do
|
|
428
473
|
// not inspect or copy that store; delegate native-provider auth to Pi.
|
|
@@ -585,13 +630,20 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
585
630
|
let effectivePolicy = (override === 'standard' || override === 'unsafe') ? override : spec.permissionPolicy;
|
|
586
631
|
// Pi resta sempre 'standard': normalizeManagedSpec rifiuta gia' unsafe nello spec
|
|
587
632
|
// dell'engine, ma l'override PER-CELL bypasserebbe quel check -> clamp esplicito.
|
|
588
|
-
if (spec.client === 'pi') effectivePolicy = 'standard';
|
|
633
|
+
if (spec.client === 'pi' || spec.client === 'shell') effectivePolicy = 'standard';
|
|
589
634
|
info.permissionPolicy = effectivePolicy;
|
|
590
635
|
if (effectivePolicy === 'unsafe') {
|
|
591
636
|
if (spec.client === 'claude') args.push('--dangerously-skip-permissions');
|
|
592
637
|
if (spec.client === 'codex' || spec.client === 'codex-vl') args.push('--dangerously-bypass-approvals-and-sandbox');
|
|
593
638
|
}
|
|
594
|
-
|
|
639
|
+
let shellOneShot = false;
|
|
640
|
+
if (spec.client === 'shell') {
|
|
641
|
+
const raw = cell?.commands && typeof cell.commands[engineId] === 'string'
|
|
642
|
+
? cell.commands[engineId] : '';
|
|
643
|
+
shellOneShot = raw.trim().length > 0;
|
|
644
|
+
if (shellOneShot) args.push('-lc', raw);
|
|
645
|
+
else args.push(...shellLoginArgs(info.binary));
|
|
646
|
+
} else if (spec.client === 'claude') {
|
|
595
647
|
if (spec.provider === 'native') {
|
|
596
648
|
if (engine.rc !== false) args.push('--remote-control', `Cloud_${cell.id}`);
|
|
597
649
|
} else if (profile.providerEnv) {
|
|
@@ -708,7 +760,7 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
708
760
|
if (model) args.push('--model', model);
|
|
709
761
|
if (spec.provider === 'alibaba-token-plan' && model === 'qwen3.8-max-preview') args.push('--thinking', 'xhigh');
|
|
710
762
|
}
|
|
711
|
-
if (cell?.prompt) args.push(cell.prompt);
|
|
763
|
+
if (spec.client !== 'shell' && cell?.prompt) args.push(cell.prompt);
|
|
712
764
|
let command = info.binary;
|
|
713
765
|
if (needsExplicitNode(info.binary, cfg.platform || process.platform, cfg.env || process.env)) {
|
|
714
766
|
command = cfg.nodeExecPath || process.execPath;
|
|
@@ -716,6 +768,7 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
716
768
|
}
|
|
717
769
|
return { ok: true, info, engine: {
|
|
718
770
|
...engine, command, args, env, promptMode: 'managed-argv', clientBinary: info.binary,
|
|
771
|
+
...(spec.client === 'shell' ? { shellOneShot } : {}),
|
|
719
772
|
} };
|
|
720
773
|
}
|
|
721
774
|
|
|
@@ -724,7 +777,7 @@ function publicCatalog() {
|
|
|
724
777
|
id: p.id, client: p.client, clientLabel: CLIENT_LABELS[p.client], provider: p.provider,
|
|
725
778
|
credentialProfile: p.credentialProfile || '', label: p.label, protocol: p.protocol,
|
|
726
779
|
auth: p.auth, endpoint: p.endpoint || '', model: p.model || '', models: [...(p.models || [])],
|
|
727
|
-
protocols: [...(p.protocols || [p.protocol])], supportsUnsafe:
|
|
780
|
+
protocols: [...(p.protocols || [p.protocol])], supportsUnsafe: !['pi', 'shell'].includes(p.client), requiresModel: !!p.requiresModel || !!p.custom,
|
|
728
781
|
permissionPolicyDefault: p.client === 'claude' ? 'unsafe' : 'standard',
|
|
729
782
|
rc: !!p.rc, custom: !!p.custom, default: !!p.default, notice: p.notice || '',
|
|
730
783
|
credentialEnv: p.auth === 'dynamic' ? !!p.credentialEnv : (ENV_KEY_RE.test(p.auth || '') ? p.auth : false),
|
|
@@ -736,8 +789,8 @@ module.exports = {
|
|
|
736
789
|
CATALOG, OLLAMA_CLOUD_MODELS, OLLAMA_CONTEXT, ALIBABA_TOKEN_PLAN_MODELS,
|
|
737
790
|
ALIBABA_CODEX_MODELS, ALIBABA_TOKEN_PLAN_CONTEXT, ALIBABA_PI_MODELS,
|
|
738
791
|
CLIENT_LABELS, normalizeManagedSpec,
|
|
739
|
-
defaultDefinitions, describeManaged, describeCatalogCredential, discoverOllamaModels, resolveManagedEngine, needsExplicitNode,
|
|
792
|
+
defaultDefinitions, defaultShellEngine, describeManaged, describeCatalogCredential, discoverOllamaModels, resolveManagedEngine, needsExplicitNode,
|
|
740
793
|
discoverPiModels, parseEnvFile, parseProviderShellFile, findBinary, publicCatalog, writePiProviderExtension,
|
|
741
794
|
providerKeyPaths, parseProviderKeyFiles, credentialSources, credential,
|
|
742
|
-
ensureKimiClaudeConfig, ensureAlibabaClaudeConfig, ENV_KEY_RE,
|
|
795
|
+
ensureKimiClaudeConfig, ensureAlibabaClaudeConfig, resolveInteractiveShell, shellLoginArgs, ENV_KEY_RE,
|
|
743
796
|
};
|
package/lib/fleet/routes.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const express = require('express');
|
|
3
|
+
const { codeOf, phaseOf } = require('./causes.js');
|
|
3
4
|
|
|
4
5
|
// Fallback difensivo per un adapter builtin incompleto nei test. Il provider di
|
|
5
6
|
// prodotto dichiara sempre la propria capability list.
|
|
@@ -60,10 +61,23 @@ function fleetRoutes(fleetP, cfg = {}) {
|
|
|
60
61
|
action: opts.action, cell: safeCell(req.body), errno: match[1], client: match[2], status: e.status || 500,
|
|
61
62
|
});
|
|
62
63
|
else emit('warn', 'FLEET_ACTION_FAILED', 'Fleet action failed', {
|
|
63
|
-
action: opts.action, cell: safeCell(req.body), state: 'failed',
|
|
64
|
+
action: opts.action, cell: safeCell(req.body), state: 'failed',
|
|
65
|
+
status: e.status || 500,
|
|
66
|
+
// Cause-preserving (T4): bounded enum only (lib/fleet/causes.js). The
|
|
67
|
+
// cause triple is {status, code, phase}; no free message, cwd/path,
|
|
68
|
+
// argv, env, prompt, token or credential ever enters here. Untagged /
|
|
69
|
+
// legacy errors degrade to the bounded UNKNOWN fallback.
|
|
70
|
+
code: codeOf(e.fleetCode), phase: phaseOf(e.fleetPhase),
|
|
64
71
|
});
|
|
65
72
|
}
|
|
66
|
-
res.status(e.status || 500).json({
|
|
73
|
+
res.status(e.status || 500).json({
|
|
74
|
+
error: String(e.message || e),
|
|
75
|
+
...(e.data || {}),
|
|
76
|
+
// Structured, bounded cause on the HTTP error too: stable enum only,
|
|
77
|
+
// never cwd/path, argv, env, prompt, token or credentials. Untagged
|
|
78
|
+
// errors (no fleetCode) keep the historical response shape.
|
|
79
|
+
...(e.fleetCode ? { code: codeOf(e.fleetCode), phase: phaseOf(e.fleetPhase) } : {}),
|
|
80
|
+
});
|
|
67
81
|
}
|
|
68
82
|
};
|
|
69
83
|
|
package/lib/fleet/runtime.js
CHANGED
|
@@ -85,7 +85,7 @@ function createBuiltinRuntime(ctx) {
|
|
|
85
85
|
const remembered = c.permissionPolicies
|
|
86
86
|
&& (c.permissionPolicies[c.engine] === 'standard' || c.permissionPolicies[c.engine] === 'unsafe')
|
|
87
87
|
? c.permissionPolicies[c.engine] : null;
|
|
88
|
-
const effectivePolicy = engineDef?.managed?.client
|
|
88
|
+
const effectivePolicy = ['pi', 'shell'].includes(engineDef?.managed?.client)
|
|
89
89
|
? 'standard'
|
|
90
90
|
: (remembered || engineDefault || '');
|
|
91
91
|
return {
|
|
@@ -149,16 +149,19 @@ function createBuiltinRuntime(ctx) {
|
|
|
149
149
|
let launchEngine = engine;
|
|
150
150
|
if (engine.managed) {
|
|
151
151
|
const resolved = resolveManagedEngine(engine, cell, { ...cfg, home });
|
|
152
|
-
if (!resolved.ok)
|
|
152
|
+
if (!resolved.ok) {
|
|
153
|
+
const code = engine.managed.client === 'shell' ? 'SHELL_NOT_AVAILABLE' : 'ENGINE_UNCONFIGURED';
|
|
154
|
+
throw httpError(400, `engine managed non configurato (${engine.id}): ${resolved.reason}`, null, { phase: 'preflight', code });
|
|
155
|
+
}
|
|
153
156
|
launchEngine = resolved.engine;
|
|
154
157
|
}
|
|
155
158
|
|
|
156
159
|
// (2) trust del command PRIMA di lanciare
|
|
157
160
|
const trust = validateCommandTrust(launchEngine.command);
|
|
158
|
-
if (!trust.ok) throw httpError(400, `command non trusted (${launchEngine.command}): ${trust.reason}
|
|
161
|
+
if (!trust.ok) throw httpError(400, `command non trusted (${launchEngine.command}): ${trust.reason}`, null, { phase: 'preflight', code: 'COMMAND_UNTRUSTED' });
|
|
159
162
|
// (3) cwd reale sotto la home
|
|
160
163
|
const realCwd = resolveCwd(cell.cwd, home);
|
|
161
|
-
if (!realCwd) throw httpError(400, `cwd non valida (deve esistere sotto la home): ${cell.cwd}
|
|
164
|
+
if (!realCwd) throw httpError(400, `cwd non valida (deve esistere sotto la home): ${cell.cwd}`, null, { phase: 'preflight', code: 'CWD_INVALID' });
|
|
162
165
|
|
|
163
166
|
// (4)+(5) argv diretto (no shell). Every cell goes through the private
|
|
164
167
|
// broker-backed supervisor: credentials never enter tmux state/argv and a
|
|
@@ -168,32 +171,46 @@ function createBuiltinRuntime(ctx) {
|
|
|
168
171
|
// #5: elimina la race di riuso del nome sessione tra waitAlive e paste).
|
|
169
172
|
const readyMs = cfg.launchReadyMs != null ? cfg.launchReadyMs : 500;
|
|
170
173
|
const child = composeClientInvocation(launchEngine, cell);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
enabled: true,
|
|
181
|
-
initialReadyMs: Math.max(50, Math.min(30000, Number(readyMs) || 500)),
|
|
182
|
-
restartDelayMs: Math.max(50, Math.min(60000, Number(cfg.cellRestartDelayMs) || 1000)),
|
|
183
|
-
maxRestartDelayMs: Math.max(100, Math.min(300000, Number(cfg.cellMaxRestartDelayMs) || 60000)),
|
|
184
|
-
resetAfterMs: Math.max(1000, Math.min(3600000, Number(cfg.cellRestartResetMs) || 30000)),
|
|
185
|
-
rapidWindowMs: Math.max(1000, Math.min(3600000, Number(cfg.cellRapidWindowMs) || 60000)),
|
|
186
|
-
maxRapidRestarts: Math.max(1, Math.min(100, Number(cfg.cellMaxRapidRestarts) || 8)),
|
|
187
|
-
},
|
|
188
|
-
...(launchEngine.promptMode === 'send-keys' && cell.prompt ? {
|
|
189
|
-
restartPrompt: {
|
|
190
|
-
tmuxBin,
|
|
191
|
-
tmuxSession: cell.tmuxSession,
|
|
192
|
-
prompt: cell.prompt,
|
|
193
|
-
readyMs: Math.max(0, Math.min(30000, Number(cfg.sendKeysReadyMs) || readyMs)),
|
|
174
|
+
let ticket;
|
|
175
|
+
try {
|
|
176
|
+
ticket = await launchBroker.issue({
|
|
177
|
+
command: child.command,
|
|
178
|
+
args: child.args,
|
|
179
|
+
env: {
|
|
180
|
+
...minimalEnv(),
|
|
181
|
+
...launchEngine.env,
|
|
182
|
+
NEXUSCREW_MCP_SESSION: cell.tmuxSession,
|
|
194
183
|
},
|
|
195
|
-
|
|
196
|
-
|
|
184
|
+
supervise: {
|
|
185
|
+
enabled: !launchEngine.shellOneShot,
|
|
186
|
+
initialReadyMs: Math.max(50, Math.min(30000, Number(readyMs) || 500)),
|
|
187
|
+
restartDelayMs: Math.max(50, Math.min(60000, Number(cfg.cellRestartDelayMs) || 1000)),
|
|
188
|
+
maxRestartDelayMs: Math.max(100, Math.min(300000, Number(cfg.cellMaxRestartDelayMs) || 60000)),
|
|
189
|
+
resetAfterMs: Math.max(1000, Math.min(3600000, Number(cfg.cellRestartResetMs) || 30000)),
|
|
190
|
+
rapidWindowMs: Math.max(1000, Math.min(3600000, Number(cfg.cellRapidWindowMs) || 60000)),
|
|
191
|
+
maxRapidRestarts: Math.max(1, Math.min(100, Number(cfg.cellMaxRapidRestarts) || 8)),
|
|
192
|
+
},
|
|
193
|
+
...(launchEngine.promptMode === 'send-keys' && cell.prompt ? {
|
|
194
|
+
restartPrompt: {
|
|
195
|
+
tmuxBin,
|
|
196
|
+
tmuxSession: cell.tmuxSession,
|
|
197
|
+
prompt: cell.prompt,
|
|
198
|
+
readyMs: Math.max(0, Math.min(30000, Number(cfg.sendKeysReadyMs) || readyMs)),
|
|
199
|
+
},
|
|
200
|
+
} : {}),
|
|
201
|
+
});
|
|
202
|
+
} catch (brokerErr) {
|
|
203
|
+
const bmsg = String((brokerErr && brokerErr.message) || brokerErr);
|
|
204
|
+
const bcode = /too large/i.test(bmsg) ? 'LAUNCH_BROKER_PAYLOAD'
|
|
205
|
+
: /closed/i.test(bmsg) ? 'LAUNCH_BROKER_CLOSED'
|
|
206
|
+
: /unsafe launch broker/i.test(bmsg) ? 'LAUNCH_BROKER_UNSAFE'
|
|
207
|
+
: 'LAUNCH_BROKER_FAILED';
|
|
208
|
+
const bpublic = bcode === 'LAUNCH_BROKER_PAYLOAD' ? 'launch broker payload rifiutato'
|
|
209
|
+
: bcode === 'LAUNCH_BROKER_CLOSED' ? 'launch broker non disponibile'
|
|
210
|
+
: bcode === 'LAUNCH_BROKER_UNSAFE' ? 'launch broker non sicuro'
|
|
211
|
+
: 'launch broker failed';
|
|
212
|
+
throw httpError(500, bpublic, null, { phase: 'launch-broker', code: bcode });
|
|
213
|
+
}
|
|
197
214
|
const tmuxLaunchEngine = {
|
|
198
215
|
command: process.execPath,
|
|
199
216
|
args: [path.join(__dirname, 'cell-exec.js'), '--socket', ticket.socketPath, '--nonce', ticket.nonce],
|
|
@@ -204,14 +221,27 @@ function createBuiltinRuntime(ctx) {
|
|
|
204
221
|
// Mantieni il pane morto solo durante la finestra di readiness: permette di
|
|
205
222
|
// catturare un errore reale del client senza lasciare una sessione fantasma.
|
|
206
223
|
// Il separatore e' interpretato da tmux (execFile argv diretto), non da shell.
|
|
207
|
-
|
|
224
|
+
if (!launchEngine.shellOneShot) {
|
|
225
|
+
argv.push(';', 'set-option', '-w', '-t', `=${cell.tmuxSession}:`, 'remain-on-exit', 'on');
|
|
226
|
+
}
|
|
208
227
|
const launch = await tmuxExec(tmuxBin, argv, { env: minimalEnv() });
|
|
209
228
|
if (launch.err) {
|
|
210
229
|
// Redazione (§9h): lo stderr di tmux puo' ecoare argv/env del comando lanciato.
|
|
211
|
-
const
|
|
230
|
+
const dup = /duplicate session/i.test(launch.stderr);
|
|
231
|
+
const why = dup
|
|
212
232
|
? 'sessione già in esecuzione'
|
|
213
233
|
: `tmux new-session failed: ${redactSecrets(launch.stderr.trim() || launch.err.message, launchEngine, cell)}`;
|
|
214
|
-
throw httpError(
|
|
234
|
+
throw httpError(dup ? 409 : 500, why, null,
|
|
235
|
+
{ phase: 'new-session', code: dup ? 'SESSION_DUPLICATE' : 'NEW_SESSION_FAILED' });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Un comando Shell valorizzato e' intenzionalmente one-shot: l'uscita del
|
|
239
|
+
// processo, anche immediata o non-zero, e' il risultato del comando e non
|
|
240
|
+
// un crash del client Fleet. Non si mantiene il pane e non si abilita il
|
|
241
|
+
// supervisore; la cella torna inattiva quando la shell termina.
|
|
242
|
+
if (launchEngine.shellOneShot) {
|
|
243
|
+
cache = { ...cache, at: 0 };
|
|
244
|
+
return { ok: true, cell: cellId, session: cell.tmuxSession, prompt: null, oneShot: true };
|
|
215
245
|
}
|
|
216
246
|
|
|
217
247
|
// `tmux new-session -d` can return 0 even when the launched CLI exits a
|
|
@@ -235,7 +265,13 @@ function createBuiltinRuntime(ctx) {
|
|
|
235
265
|
cache = { ...cache, at: 0 };
|
|
236
266
|
const client = path.basename(launchEngine.clientBinary || launchEngine.command || 'client');
|
|
237
267
|
const status = Number.isInteger(readiness.status) ? ` (exit ${readiness.status})` : '';
|
|
238
|
-
|
|
268
|
+
// Cause-preserving (T4): distinguish a cell-client spawn failure (the
|
|
269
|
+
// captured pane carries the stable 'cell spawn failed:' marker produced by
|
|
270
|
+
// cell-exec.js) from a generic early exit. Both stay on the readiness
|
|
271
|
+
// surface; the spawn branch keeps CELL_SPAWN_FAILED sanitized downstream.
|
|
272
|
+
const isSpawn = /cell spawn failed:/.test(diagnostic);
|
|
273
|
+
throw httpError(500, `client ${client} terminato subito${status}: ${diagnostic || 'verifica login, provider, modello e argomenti dell\'engine'}`, null,
|
|
274
|
+
isSpawn ? { phase: 'spawn-client', code: 'SPAWN_CLIENT_FAILED' } : { phase: 'readiness', code: 'CLIENT_EARLY_EXIT' });
|
|
239
275
|
}
|
|
240
276
|
if (readiness.target) {
|
|
241
277
|
await tmuxExec(tmuxBin,
|