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.
@@ -38,7 +38,7 @@ const inquirer = require('inquirer');
38
38
 
39
39
  const {
40
40
  mcpConfig: { addMcpEntry },
41
- } = require('byan-platform-config');
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
- * Uses POSIX `command -v` (and `where` on Windows). The runner is injectable.
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 = execSync, platform = process.platform } = {}) {
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('byan-platform-config');
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
- // but the merge signature accepts it; a localhost placeholder keeps it valid.
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: 'http://localhost:3737' });
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 });