create-byan-agent 2.59.2 → 2.60.0
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/install/bin/create-byan-agent-v2.js +94 -44
- package/install/lib/api-defaults.js +111 -0
- package/install/lib/byan-leantime-integration.js +1 -1
- package/install/lib/byan-web-integration.js +10 -3
- package/install/lib/claude-native-setup.js +1 -1
- package/install/lib/codex-native-setup.js +55 -20
- package/install/lib/gdoc-setup.js +1 -1
- package/install/lib/install-engine.js +490 -58
- package/install/lib/mcp-extensions/index.js +1 -1
- package/install/lib/native-helper.js +13 -2
- package/install/lib/ownership.js +489 -0
- package/install/lib/platforms/claude-code.js +8 -4
- package/install/lib/resolve-binary.js +398 -0
- package/install/lib/rtk-integration.js +9 -2
- package/install/lib/stt/engine.js +5 -6
- package/install/lib/target-user.js +373 -0
- package/install/lib/yanstaller/agent-launcher.js +7 -16
- package/install/package.json +1 -1
- package/install/packages/platform-config/lib/token-prompt.js +7 -1
- package/install/src/webui/api.js +14 -1
- package/install/src/webui/public/app.js +39 -0
- package/package.json +1 -2
- package/update-byan-agent/lib/migrate-mcp-config.js +1 -1
- package/update-byan-agent/package.json +0 -1
|
@@ -38,7 +38,7 @@ const inquirer = require('inquirer');
|
|
|
38
38
|
|
|
39
39
|
const {
|
|
40
40
|
mcpConfig: { addMcpEntry },
|
|
41
|
-
} = require('
|
|
41
|
+
} = require('../../packages/platform-config');
|
|
42
42
|
const { whitelistMcpServer } = require('../settings-local');
|
|
43
43
|
|
|
44
44
|
const EXTENSIONS = [];
|
|
@@ -14,12 +14,23 @@ const { execSync } = require('child_process');
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const os = require('os');
|
|
16
16
|
const path = require('path');
|
|
17
|
+
const { commandExists: sharedCommandExists } = require('./resolve-binary');
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* commandExists(cmd) -> boolean. Is an executable resolvable on PATH?
|
|
20
|
-
*
|
|
21
|
+
*
|
|
22
|
+
* La sonde par defaut ne passe plus par un shell. Elle delegue au resolveur
|
|
23
|
+
* partage (install/lib/resolve-binary.js), qui parcourt le PATH en JavaScript
|
|
24
|
+
* et connait PATHEXT sous Windows. Motif mesure le 2026-08-11 : trois sondes
|
|
25
|
+
* concurrentes coexistaient dans install/ et se contredisaient, et celle-ci
|
|
26
|
+
* lancait un shell pour repondre a une question que le systeme de fichiers
|
|
27
|
+
* suffit a trancher.
|
|
28
|
+
*
|
|
29
|
+
* Le lanceur reste injectable pour les tests qui simulent deja un shell : quand
|
|
30
|
+
* `run` est fourni explicitement, on garde l'ancien chemin.
|
|
21
31
|
*/
|
|
22
|
-
function commandExists(cmd, { run =
|
|
32
|
+
function commandExists(cmd, { run = null, platform = process.platform, env = process.env } = {}) {
|
|
33
|
+
if (!run) return sharedCommandExists(cmd, { platform, env });
|
|
23
34
|
const probe = platform === 'win32' ? `where ${cmd}` : `command -v ${cmd}`;
|
|
24
35
|
try {
|
|
25
36
|
run(probe, { stdio: 'pipe' });
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ownership.js -- reparation du proprietaire des fichiers poses par l installateur.
|
|
4
|
+
//
|
|
5
|
+
// Le probleme mesure le 2026-08-11 sur un serveur reel : le dossier parent du
|
|
6
|
+
// projet est en yan:docker, mais _byan/ cree sous elevation ressort en root:root.
|
|
7
|
+
// L utilisateur ne peut plus modifier ce que l installateur a ecrit.
|
|
8
|
+
//
|
|
9
|
+
// Quatre faits mesures sur cette machine (Node v26.2.0, Linux) portent le code :
|
|
10
|
+
// 1. sans le bit setgid sur le dossier, un fichier cree dedans prend le groupe
|
|
11
|
+
// PRIMAIRE de son createur, pas celui du dossier ;
|
|
12
|
+
// 2. avec le setgid pose, le fichier herite du groupe mais ressort en 644 car
|
|
13
|
+
// le umask 022 retire l ecriture au groupe -> il faut umask(0o002) ;
|
|
14
|
+
// 3. le setgid se propage aux sous-dossiers crees ensuite : une seule pose a
|
|
15
|
+
// la racine suffit pour l arborescence creee apres ;
|
|
16
|
+
// 4. fs.constants n expose aucune constante S_ISGID, d ou le litteral 0o2000.
|
|
17
|
+
//
|
|
18
|
+
// Toutes les dependances a effet de bord (fs, platform, exec, umask) sont
|
|
19
|
+
// injectees : c est ce qui rend le test possible sans privilege ni machine hote.
|
|
20
|
+
|
|
21
|
+
const path = require('path');
|
|
22
|
+
|
|
23
|
+
// Bit setgid. WHY un litteral : fs.constants n expose aucune constante
|
|
24
|
+
// S_ISGID / S_ISUID / S_ISVTX (verifie sur Node v26.2.0), donc rien a importer.
|
|
25
|
+
const SETGID = 0o2000;
|
|
26
|
+
|
|
27
|
+
// Umask a poser pour que les fichiers crees ensuite gardent l ecriture au
|
|
28
|
+
// groupe (fait mesure numero 2).
|
|
29
|
+
const GROUP_WRITABLE_UMASK = 0o002;
|
|
30
|
+
|
|
31
|
+
// Codes d erreur qui signalent une ABSENCE DE SUJET plutot qu une panne : le
|
|
32
|
+
// montage ne porte pas de proprietaire POSIX (exFAT, NTFS, /mnt/c en WSL sans
|
|
33
|
+
// metadata).
|
|
34
|
+
//
|
|
35
|
+
// EPERM N EN FAIT PLUS PARTIE. Il portait deux causes que rien ne distinguait :
|
|
36
|
+
// un montage qui refuse le concept de proprietaire, et un processus qui n a pas
|
|
37
|
+
// le privilege de donner le fichier. Mesure du 2026-08-11 : un chown vers uid 0
|
|
38
|
+
// depuis un compte ordinaire rendait outcome 'not-applicable' avec la raison
|
|
39
|
+
// 'montage-sans-proprietaire-posix' sur un tmpfs parfaitement POSIX. Le rapport
|
|
40
|
+
// disait "rien a faire" la ou il fallait dire "tu n as pas les droits".
|
|
41
|
+
const NO_POSIX_OWNER_CODES = ['ENOTSUP', 'EOPNOTSUPP', 'EINVAL', 'ENOSYS'];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* De quelle nature est cette erreur : 'not-applicable' ou 'failed' ?
|
|
45
|
+
*
|
|
46
|
+
* EPERM se tranche sur le contexte, parce que le noyau rend le meme code pour
|
|
47
|
+
* les deux causes :
|
|
48
|
+
* - le processus n est pas root ET vise un autre uid que le sien -> le systeme
|
|
49
|
+
* lui refuse le geste. C est un ECHEC, avec une cause nommable.
|
|
50
|
+
* - le processus est root (ou vise son propre uid) et se fait quand meme
|
|
51
|
+
* refuser -> c est le montage qui n en veut pas. Absence de sujet.
|
|
52
|
+
*/
|
|
53
|
+
function classifyError(err, { euid = null, targetUid = null } = {}) {
|
|
54
|
+
if (!err) return null;
|
|
55
|
+
if (NO_POSIX_OWNER_CODES.indexOf(err.code) !== -1) return 'not-applicable';
|
|
56
|
+
if (err.code !== 'EPERM') return 'failed';
|
|
57
|
+
if (euid !== null && euid !== 0 && targetUid !== null && targetUid !== euid) return 'failed';
|
|
58
|
+
return 'not-applicable';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isNoPosixOwner(err) {
|
|
62
|
+
return !!err && NO_POSIX_OWNER_CODES.indexOf(err.code) !== -1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function defaultFs() {
|
|
66
|
+
return require('fs');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// exec(fichier, args) -> stdout en utf8, leve si la commande echoue.
|
|
70
|
+
function defaultExec(file, args) {
|
|
71
|
+
const { execFileSync } = require('child_process');
|
|
72
|
+
return execFileSync(file, args, {
|
|
73
|
+
encoding: 'utf8',
|
|
74
|
+
timeout: 5000,
|
|
75
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// WHY une enveloppe et pas process.umask directement : passer la methode
|
|
80
|
+
// detachee la coupe de son recepteur, et l injection doit rester remplacable
|
|
81
|
+
// par une fonction de test qui ne touche pas au processus jest.
|
|
82
|
+
function defaultUmask(mask) {
|
|
83
|
+
return process.umask(mask);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ownershipOf(chemin) -> { uid, gid, mode } ou null quand le chemin est absent.
|
|
87
|
+
// Sert au court-circuit du parcours et aux verifications de test.
|
|
88
|
+
function ownershipOf(targetPath, options) {
|
|
89
|
+
const opts = options || {};
|
|
90
|
+
const fs = opts.fs || defaultFs();
|
|
91
|
+
try {
|
|
92
|
+
const st = fs.lstatSync(targetPath);
|
|
93
|
+
return { uid: st.uid, gid: st.gid, mode: st.mode & 0o7777 };
|
|
94
|
+
} catch (_err) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function lstatOrNull(fs, p) {
|
|
100
|
+
try {
|
|
101
|
+
return fs.lstatSync(p);
|
|
102
|
+
} catch (_err) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function notApplicable(reason, extra) {
|
|
108
|
+
const out = {
|
|
109
|
+
outcome: 'not-applicable',
|
|
110
|
+
changed: 0,
|
|
111
|
+
skipped: 0,
|
|
112
|
+
errors: [],
|
|
113
|
+
reason: reason,
|
|
114
|
+
};
|
|
115
|
+
if (extra) Object.assign(out, extra);
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ensureOwnership(racine, cible, options)
|
|
120
|
+
// cible = { uid, gid } (la sortie de install/lib/target-user.js)
|
|
121
|
+
// -> { outcome: 'ok' | 'not-applicable' | 'failed', changed, skipped, errors }
|
|
122
|
+
//
|
|
123
|
+
// options.recursive (defaut true) descend dans l arborescence
|
|
124
|
+
// options.dryRun (defaut false) compte sans appeler chown
|
|
125
|
+
// options.skipUnchanged (defaut true) court-circuit quand le proprietaire colle deja
|
|
126
|
+
function ensureOwnership(rootPath, target, options) {
|
|
127
|
+
const opts = options || {};
|
|
128
|
+
const fs = opts.fs || defaultFs();
|
|
129
|
+
const platform = opts.platform || process.platform;
|
|
130
|
+
const recursive = opts.recursive !== false;
|
|
131
|
+
const dryRun = opts.dryRun === true;
|
|
132
|
+
const skipUnchanged = opts.skipUnchanged !== false;
|
|
133
|
+
// Pose g+w (et g+x sur les dossiers) pendant le parcours. Utilise seulement
|
|
134
|
+
// quand un groupe partage est demande : sans lui, elargir les droits serait
|
|
135
|
+
// un effet de bord que personne n a demande.
|
|
136
|
+
const groupWritable = opts.groupWritable === true;
|
|
137
|
+
// L uid effectif sert a trancher EPERM. Injectable pour les tests ; null sur
|
|
138
|
+
// une plateforme sans geteuid, auquel cas EPERM retombe sur 'not-applicable'.
|
|
139
|
+
const euid = Object.prototype.hasOwnProperty.call(opts, 'euid')
|
|
140
|
+
? opts.euid
|
|
141
|
+
: (typeof process.geteuid === 'function' ? process.geteuid() : null);
|
|
142
|
+
|
|
143
|
+
if (platform === 'win32') {
|
|
144
|
+
return notApplicable('windows-sans-proprietaire-posix');
|
|
145
|
+
}
|
|
146
|
+
if (!target || target.uid === null || target.uid === undefined) {
|
|
147
|
+
return notApplicable('cible-inconnue');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const uid = target.uid;
|
|
151
|
+
const result = { outcome: 'ok', changed: 0, skipped: 0, errors: [] };
|
|
152
|
+
|
|
153
|
+
function gidFor(st) {
|
|
154
|
+
// Cible sans gid : on garde le groupe en place et on ne corrige que l uid.
|
|
155
|
+
if (target.gid === null || target.gid === undefined) return st.gid;
|
|
156
|
+
return target.gid;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function record(err, p) {
|
|
160
|
+
result.errors.push({ path: p, code: err.code || null, message: err.message });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Le groupe partage n a de sens que si le groupe peut ECRIRE. Le setgid pose
|
|
164
|
+
// a la racine gouverne ce qui sera cree ensuite ; l existant, lui, garde le
|
|
165
|
+
// mode qu il avait — 644 sur les fichiers, 755 sur les dossiers. Un groupe
|
|
166
|
+
// partage sans droit d ecriture sur l existant ne donne accces a rien.
|
|
167
|
+
function ouvrirAuGroupe(p, st) {
|
|
168
|
+
if (!groupWritable || typeof fs.chmodSync !== 'function') return;
|
|
169
|
+
const actuel = Number(st.mode) & 0o7777;
|
|
170
|
+
// g+w partout ; g+x en plus sur un dossier, sinon le groupe ne peut pas le
|
|
171
|
+
// traverser meme en ayant le droit d y ecrire.
|
|
172
|
+
const vise = actuel | 0o020 | (st.isDirectory && st.isDirectory() ? 0o010 : 0);
|
|
173
|
+
if (vise === actuel) return;
|
|
174
|
+
try {
|
|
175
|
+
fs.chmodSync(p, vise);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
record(err, p);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Applique le proprietaire a une entree. Rend l erreur rencontree (ou null),
|
|
182
|
+
// pour que l appelant puisse decider d abandonner sur la sonde racine.
|
|
183
|
+
function applyOne(p, st) {
|
|
184
|
+
const gid = gidFor(st);
|
|
185
|
+
const isLinkTot = typeof st.isSymbolicLink === 'function' && st.isSymbolicLink();
|
|
186
|
+
// chmod suit le lien : on ne touche pas au mode au travers d un lien.
|
|
187
|
+
if (!isLinkTot) ouvrirAuGroupe(p, st);
|
|
188
|
+
if (skipUnchanged && st.uid === uid && st.gid === gid) {
|
|
189
|
+
result.skipped++;
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
if (dryRun) {
|
|
193
|
+
result.changed++;
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
const isLink = typeof st.isSymbolicLink === 'function' && st.isSymbolicLink();
|
|
197
|
+
// Un lien symbolique se corrige avec lchown, sinon chown suivrait le lien et
|
|
198
|
+
// sortirait de l arborescence installee.
|
|
199
|
+
if (isLink && typeof fs.lchownSync !== 'function') {
|
|
200
|
+
result.skipped++;
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
if (isLink) fs.lchownSync(p, uid, gid);
|
|
205
|
+
else fs.chownSync(p, uid, gid);
|
|
206
|
+
result.changed++;
|
|
207
|
+
return null;
|
|
208
|
+
} catch (err) {
|
|
209
|
+
record(err, p);
|
|
210
|
+
return err;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const rootStat = lstatOrNull(fs, rootPath);
|
|
215
|
+
if (!rootStat) {
|
|
216
|
+
result.outcome = 'failed';
|
|
217
|
+
result.errors.push({ path: rootPath, code: 'ENOENT', message: 'chemin absent : ' + rootPath });
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// UNE RACINE SYMBOLIQUE EST REFUSEE, PAS TRAITEE A MOITIE.
|
|
222
|
+
//
|
|
223
|
+
// lstat d'un lien rend isDirectory() faux : le parcours ne demarrait pas, et
|
|
224
|
+
// la fonction rendait 'ok' apres avoir corrige une seule entree — le lien.
|
|
225
|
+
// L'arborescence reelle, celle ou les etapes d'installation ont ecrit,
|
|
226
|
+
// gardait son proprietaire. Un 'ok' portant sur une entree quand on en attend
|
|
227
|
+
// des milliers est le pire des deux mondes : ni corrige, ni signale.
|
|
228
|
+
// L'appelant resout le chemin (fs.realpathSync) avant d'appeler.
|
|
229
|
+
if (typeof rootStat.isSymbolicLink === 'function' && rootStat.isSymbolicLink()) {
|
|
230
|
+
result.outcome = 'failed';
|
|
231
|
+
result.reason = 'racine-symbolique';
|
|
232
|
+
result.errors.push({
|
|
233
|
+
path: rootPath,
|
|
234
|
+
code: 'ESYMLINK',
|
|
235
|
+
message: 'la racine est un lien symbolique : resoudre le chemin avant la reprise (fs.realpathSync)',
|
|
236
|
+
});
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Sonde : la racine sert de test du montage. Une erreur de la famille
|
|
241
|
+
// "pas de proprietaire POSIX" ici vaut pour toute l arborescence, on arrete
|
|
242
|
+
// avant de lancer des milliers d appels qui echoueront de la meme facon.
|
|
243
|
+
const rootErr = applyOne(rootPath, rootStat);
|
|
244
|
+
if (rootErr) {
|
|
245
|
+
const nature = classifyError(rootErr, { euid: euid, targetUid: uid });
|
|
246
|
+
if (nature === 'not-applicable') {
|
|
247
|
+
return notApplicable('montage-sans-proprietaire-posix', {
|
|
248
|
+
errors: result.errors.slice(),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (rootErr.code === 'EPERM') {
|
|
252
|
+
// Privilege insuffisant, et ca se dit. Poursuivre le parcours ne ferait
|
|
253
|
+
// qu accumuler la meme erreur sur chaque entree.
|
|
254
|
+
result.outcome = 'failed';
|
|
255
|
+
result.reason = 'privilege-insuffisant';
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (recursive && rootStat.isDirectory()) {
|
|
261
|
+
const stack = [rootPath];
|
|
262
|
+
while (stack.length > 0) {
|
|
263
|
+
const dir = stack.pop();
|
|
264
|
+
let names;
|
|
265
|
+
try {
|
|
266
|
+
names = fs.readdirSync(dir);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
record(err, dir);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
for (let i = 0; i < names.length; i++) {
|
|
272
|
+
const child = path.join(dir, names[i]);
|
|
273
|
+
const st = lstatOrNull(fs, child);
|
|
274
|
+
// Entree disparue en cours de parcours : aucun proprietaire a corriger,
|
|
275
|
+
// ce n est pas une panne.
|
|
276
|
+
if (!st) {
|
|
277
|
+
result.skipped++;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
// Une erreur ponctuelle est collectee et le parcours continue.
|
|
281
|
+
applyOne(child, st);
|
|
282
|
+
const isLink = typeof st.isSymbolicLink === 'function' && st.isSymbolicLink();
|
|
283
|
+
if (st.isDirectory() && !isLink) stack.push(child);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (result.errors.length === 0) {
|
|
289
|
+
result.outcome = 'ok';
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
const natures = result.errors.map(function (e) {
|
|
293
|
+
return classifyError({ code: e.code }, { euid: euid, targetUid: uid });
|
|
294
|
+
});
|
|
295
|
+
if (natures.every(function (n) { return n === 'not-applicable'; })) {
|
|
296
|
+
result.outcome = 'not-applicable';
|
|
297
|
+
result.reason = 'montage-sans-proprietaire-posix';
|
|
298
|
+
} else {
|
|
299
|
+
result.outcome = 'failed';
|
|
300
|
+
if (result.errors.some(function (e) { return e.code === 'EPERM'; })) {
|
|
301
|
+
result.reason = 'privilege-insuffisant';
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return result;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// resolveGroupGid(nom, options) -> { gid, source, warnings }
|
|
308
|
+
// Node n expose pas getgrnam : le nom se resout par la base du systeme.
|
|
309
|
+
function resolveGroupGid(groupName, options) {
|
|
310
|
+
const opts = options || {};
|
|
311
|
+
const fs = opts.fs || defaultFs();
|
|
312
|
+
const platform = opts.platform || process.platform;
|
|
313
|
+
const exec = opts.exec || defaultExec;
|
|
314
|
+
const warnings = [];
|
|
315
|
+
|
|
316
|
+
if (platform === 'darwin') {
|
|
317
|
+
// Les groupes locaux macOS vivent dans OpenDirectory, pas dans /etc/group.
|
|
318
|
+
try {
|
|
319
|
+
const out = String(exec('dscl', ['.', '-read', '/Groups/' + groupName, 'PrimaryGroupID']));
|
|
320
|
+
const m = /PrimaryGroupID:\s*(\d+)/.exec(out);
|
|
321
|
+
if (m) return { gid: Number(m[1]), source: 'dscl', warnings: warnings };
|
|
322
|
+
warnings.push('dscl a repondu sans PrimaryGroupID pour ' + groupName);
|
|
323
|
+
} catch (err) {
|
|
324
|
+
warnings.push('dscl indisponible ou en echec : ' + err.message);
|
|
325
|
+
}
|
|
326
|
+
} else {
|
|
327
|
+
// getent interroge nsswitch.conf, donc aussi la seconde source du systeme
|
|
328
|
+
// (systemd, LDAP ou SSSD selon la machine).
|
|
329
|
+
try {
|
|
330
|
+
const out = String(exec('getent', ['group', groupName])).trim();
|
|
331
|
+
const line = out.split('\n')[0] || '';
|
|
332
|
+
const fields = line.split(':');
|
|
333
|
+
if (fields.length >= 3 && /^\d+$/.test(fields[2])) {
|
|
334
|
+
return { gid: Number(fields[2]), source: 'getent', warnings: warnings };
|
|
335
|
+
}
|
|
336
|
+
warnings.push('getent a repondu sans gid exploitable pour ' + groupName);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
warnings.push('getent indisponible ou en echec : ' + err.message);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Dernier recours. WHY seulement en recours : le nsswitch.conf de la machine
|
|
343
|
+
// de reference porte "group: files [SUCCESS=merge] systemd", donc une seconde
|
|
344
|
+
// source existe et /etc/group ne suffit pas a lui seul.
|
|
345
|
+
try {
|
|
346
|
+
const content = String(fs.readFileSync('/etc/group', 'utf8'));
|
|
347
|
+
const lines = content.split('\n');
|
|
348
|
+
for (let i = 0; i < lines.length; i++) {
|
|
349
|
+
const fields = lines[i].split(':');
|
|
350
|
+
if (fields[0] === groupName && fields.length >= 3 && /^\d+$/.test(fields[2])) {
|
|
351
|
+
warnings.push('gid resolu par le recours /etc/group (source secondaire du systeme non consultee)');
|
|
352
|
+
return { gid: Number(fields[2]), source: '/etc/group', warnings: warnings };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
warnings.push('groupe ' + groupName + ' absent de /etc/group');
|
|
356
|
+
} catch (err) {
|
|
357
|
+
warnings.push('/etc/group illisible : ' + err.message);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return { gid: null, source: null, warnings: warnings };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function createGroupHint(groupName, platform) {
|
|
364
|
+
if (platform === 'darwin') {
|
|
365
|
+
return 'dseditgroup -o create ' + groupName;
|
|
366
|
+
}
|
|
367
|
+
return 'groupadd ' + groupName;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ensureSharedGroup(racine, nomDeGroupe, options)
|
|
371
|
+
// -> { outcome, gid, mode, warnings }
|
|
372
|
+
// Pose le groupe partage et le bit setgid a la racine, puis abaisse le umask.
|
|
373
|
+
// La creation du groupe reste une action d administrateur : on ne la fait pas.
|
|
374
|
+
function ensureSharedGroup(rootPath, groupName, options) {
|
|
375
|
+
const opts = options || {};
|
|
376
|
+
const fs = opts.fs || defaultFs();
|
|
377
|
+
const platform = opts.platform || process.platform;
|
|
378
|
+
const exec = opts.exec || defaultExec;
|
|
379
|
+
const umask = opts.umask || defaultUmask;
|
|
380
|
+
const euid = Object.prototype.hasOwnProperty.call(opts, 'euid')
|
|
381
|
+
? opts.euid
|
|
382
|
+
: (typeof process.geteuid === 'function' ? process.geteuid() : null);
|
|
383
|
+
const warnings = [];
|
|
384
|
+
|
|
385
|
+
if (platform === 'win32') {
|
|
386
|
+
warnings.push(
|
|
387
|
+
'droits de groupe a poser a la main : icacls ' + rootPath +
|
|
388
|
+
' /grant "' + groupName + ':(OI)(CI)M" /T'
|
|
389
|
+
);
|
|
390
|
+
return { outcome: 'not-applicable', gid: null, mode: null, warnings: warnings, reason: 'windows-sans-setgid' };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const resolved = resolveGroupGid(groupName, { fs: fs, platform: platform, exec: exec });
|
|
394
|
+
for (let i = 0; i < resolved.warnings.length; i++) warnings.push(resolved.warnings[i]);
|
|
395
|
+
|
|
396
|
+
if (resolved.gid === null) {
|
|
397
|
+
const hint = createGroupHint(groupName, platform);
|
|
398
|
+
warnings.push('groupe ' + groupName + ' introuvable ; le creer avec : ' + hint);
|
|
399
|
+
return {
|
|
400
|
+
outcome: 'failed',
|
|
401
|
+
gid: null,
|
|
402
|
+
mode: null,
|
|
403
|
+
warnings: warnings,
|
|
404
|
+
message: 'groupe ' + groupName + ' introuvable. Creer le groupe puis relancer : ' + hint,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const st = lstatOrNull(fs, rootPath);
|
|
409
|
+
|
|
410
|
+
// MEME REFUS QUE ensureOwnership, ET POUR UNE RAISON PLUS DURE ENCORE.
|
|
411
|
+
//
|
|
412
|
+
// chmod suit le lien (Linux n'a pas de lchmod), mais lstat rend le mode DU
|
|
413
|
+
// LIEN, qui vaut 0o777. Le calcul (st.mode & 0o7777) | SETGID posait donc
|
|
414
|
+
// 2777 — drwxrwsrwx — sur le vrai dossier d'installation : ouvert en ecriture
|
|
415
|
+
// au monde entier. Mesure de la revue du 2026-08-12.
|
|
416
|
+
if (st && typeof st.isSymbolicLink === 'function' && st.isSymbolicLink()) {
|
|
417
|
+
warnings.push('racine symbolique : resoudre le chemin avant de poser le groupe (fs.realpathSync)');
|
|
418
|
+
return {
|
|
419
|
+
outcome: 'failed',
|
|
420
|
+
gid: resolved.gid,
|
|
421
|
+
mode: null,
|
|
422
|
+
warnings: warnings,
|
|
423
|
+
reason: 'racine-symbolique',
|
|
424
|
+
message: 'la racine ' + rootPath + ' est un lien symbolique ; le mode lu serait celui du lien (0777), pas celui du dossier',
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (!st) {
|
|
429
|
+
return {
|
|
430
|
+
outcome: 'failed',
|
|
431
|
+
gid: resolved.gid,
|
|
432
|
+
mode: null,
|
|
433
|
+
warnings: warnings,
|
|
434
|
+
message: 'chemin absent : ' + rootPath,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
fs.chmodSync(rootPath, (st.mode & 0o7777) | SETGID);
|
|
440
|
+
// On ne change que le groupe : l uid en place est reconduit tel quel.
|
|
441
|
+
fs.chownSync(rootPath, st.uid, resolved.gid);
|
|
442
|
+
} catch (err) {
|
|
443
|
+
// Meme partage qu'ensureOwnership : EPERM depuis un compte ordinaire est un
|
|
444
|
+
// privilege manquant, pas un montage sans proprietaire.
|
|
445
|
+
const outcome = classifyError(err, { euid: euid, targetUid: st.uid });
|
|
446
|
+
warnings.push('pose du groupe partage refusee (' + (err.code || 'sans code') + ')');
|
|
447
|
+
return {
|
|
448
|
+
outcome: outcome,
|
|
449
|
+
gid: resolved.gid,
|
|
450
|
+
mode: null,
|
|
451
|
+
warnings: warnings,
|
|
452
|
+
message: err.message,
|
|
453
|
+
reason: outcome === 'not-applicable'
|
|
454
|
+
? 'montage-sans-proprietaire-posix'
|
|
455
|
+
: (err.code === 'EPERM' ? 'privilege-insuffisant' : undefined),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Le setgid seul rend un fichier en 644 sous umask 022 (fait mesure numero 2).
|
|
460
|
+
// On rend l ancienne valeur pour que l appelant la restaure apres l install.
|
|
461
|
+
let previousUmask = null;
|
|
462
|
+
try {
|
|
463
|
+
previousUmask = umask(GROUP_WRITABLE_UMASK);
|
|
464
|
+
} catch (err) {
|
|
465
|
+
warnings.push('umask non modifiable : ' + err.message);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const after = lstatOrNull(fs, rootPath);
|
|
469
|
+
return {
|
|
470
|
+
outcome: 'ok',
|
|
471
|
+
gid: resolved.gid,
|
|
472
|
+
mode: after ? after.mode & 0o7777 : null,
|
|
473
|
+
warnings: warnings,
|
|
474
|
+
source: resolved.source,
|
|
475
|
+
previousUmask: previousUmask,
|
|
476
|
+
umask: GROUP_WRITABLE_UMASK,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
module.exports = {
|
|
481
|
+
ensureOwnership,
|
|
482
|
+
classifyError,
|
|
483
|
+
ensureSharedGroup,
|
|
484
|
+
ownershipOf,
|
|
485
|
+
resolveGroupGid,
|
|
486
|
+
SETGID,
|
|
487
|
+
GROUP_WRITABLE_UMASK,
|
|
488
|
+
NO_POSIX_OWNER_CODES,
|
|
489
|
+
};
|
|
@@ -15,7 +15,8 @@ const logger = require('../utils/logger');
|
|
|
15
15
|
// merge the canonical .mcp.json writer uses (READ-MERGE-WRITE, relative paths,
|
|
16
16
|
// no secret, byan-channel inert). installDirectMCP delegates to it instead of
|
|
17
17
|
// hand-building a divergent entry with an absolute path that overwrote siblings.
|
|
18
|
-
const { mcpConfig } = require('
|
|
18
|
+
const { mcpConfig } = require('../../packages/platform-config');
|
|
19
|
+
const apiDefaults = require('../api-defaults');
|
|
19
20
|
|
|
20
21
|
const PLATFORM_NAME = 'Claude Code';
|
|
21
22
|
// Relative path discipline: the byan server entry args[0] is repo-relative
|
|
@@ -172,10 +173,13 @@ async function installDirectMCP(projectRoot, agents, config) {
|
|
|
172
173
|
// READ-MERGE-WRITE through the shared pure merge so this writer is byte-for-byte
|
|
173
174
|
// identical to the canonical .mcp.json writer: relative server path, no secret,
|
|
174
175
|
// existing mcpServers.* preserved, and the inert byan-channel entry added.
|
|
175
|
-
// apiUrl is irrelevant to the entry shape (the server resolves its own config
|
|
176
|
-
//
|
|
176
|
+
// apiUrl is irrelevant to the entry shape (the server resolves its own config,
|
|
177
|
+
// and mergeByanEntry strips BYAN_API_URL from the env it writes), but the merge
|
|
178
|
+
// signature accepts it. On passe quand meme la source unique plutot qu'un
|
|
179
|
+
// localhost code en dur : une valeur de remplissage qui ressemble a un defaut
|
|
180
|
+
// finit par etre lue comme un defaut.
|
|
177
181
|
const existingConfig = await fileUtils.readJson(configPath);
|
|
178
|
-
const merged = mcpConfig.mergeByanEntry(existingConfig, { apiUrl:
|
|
182
|
+
const merged = mcpConfig.mergeByanEntry(existingConfig, { apiUrl: apiDefaults.PROD_API_URL });
|
|
179
183
|
|
|
180
184
|
// Write updated config
|
|
181
185
|
await fileUtils.writeJson(configPath, merged, { spaces: 2 });
|