blun-king-cli 9.1.615 → 9.1.617
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 +113 -104
- package/LIESMICH.txt +56 -56
- package/README.md +89 -89
- package/bin/core-bootstrap.js +24 -8
- package/bin/launcher-runtime.js +124 -48
- package/bin/managed-plugin-selection.cjs +15 -8
- package/bin/plugin-bootstrap.js +40 -17
- package/bin/private-paths.js +10 -2
- package/bin/profile-handoff.cjs +152 -0
- package/bin/profile-launch.cjs +84 -6
- package/bin/update-lease.js +3 -1
- package/blun.mjs +3773 -1284
- package/package.json +1 -1
- package/telegram-plugin/dist/bridge.mjs +53 -13
- package/telegram-plugin/dist/mcp-server.mjs +54 -10
- package/telegram-plugin/dist/noise.mjs +9 -1
- package/worker-host.mjs +2449 -749
package/bin/launcher-runtime.js
CHANGED
|
@@ -39,6 +39,7 @@ const {
|
|
|
39
39
|
writePrivateFile,
|
|
40
40
|
} = require('./private-paths');
|
|
41
41
|
const { parseProfileLaunchArgs, resolveSelectedProfile } = require('./profile-launch.cjs');
|
|
42
|
+
const { createProfileHandoff, PROFILE_HANDOFF_READY, PROFILE_HANDOFF_EXIT } = require('./profile-handoff.cjs');
|
|
42
43
|
|
|
43
44
|
const PKG = path.resolve(__dirname, '..');
|
|
44
45
|
const PROFILE = parseProfileLaunchArgs(process.argv.slice(2), launcherModeFromArgv(process.argv));
|
|
@@ -153,7 +154,7 @@ function shouldDetachProtectedCore(options = {}) {
|
|
|
153
154
|
return platform !== 'win32' || stdinIsTTY !== true;
|
|
154
155
|
}
|
|
155
156
|
|
|
156
|
-
function spawnProtectedCore(args, env, cwd) {
|
|
157
|
+
function spawnProtectedCore(args, env, cwd, onProfileMessage) {
|
|
157
158
|
const child = spawn(
|
|
158
159
|
process.execPath,
|
|
159
160
|
[path.join(PKG, 'bin', 'core-bootstrap.js'), ...args],
|
|
@@ -167,7 +168,8 @@ function spawnProtectedCore(args, env, cwd) {
|
|
|
167
168
|
);
|
|
168
169
|
let loadedSettled = false;
|
|
169
170
|
let completedSettled = false;
|
|
170
|
-
let operatorSignal;
|
|
171
|
+
let operatorSignal;
|
|
172
|
+
let profileReady = false;
|
|
171
173
|
let resolveLoaded;
|
|
172
174
|
let resolveCompleted;
|
|
173
175
|
const loadTimer = setTimeout(() => {
|
|
@@ -201,16 +203,29 @@ function spawnProtectedCore(args, env, cwd) {
|
|
|
201
203
|
signalHandlers.set(signal, handler);
|
|
202
204
|
process.once(signal, handler);
|
|
203
205
|
}
|
|
204
|
-
child.on('message', (message) => {
|
|
205
|
-
if (message?.type !== CORE_LOADED_MESSAGE)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
206
|
+
child.on('message', (message) => {
|
|
207
|
+
if (message?.type !== CORE_LOADED_MESSAGE) {
|
|
208
|
+
if (completedSettled || !onProfileMessage) return;
|
|
209
|
+
if (message?.type === PROFILE_HANDOFF_READY && Object.keys(message).length === 1) {
|
|
210
|
+
profileReady = true;
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (!loadedSettled) return;
|
|
214
|
+
const reply = onProfileMessage(message);
|
|
215
|
+
if (reply) {
|
|
216
|
+
try { child.send(reply, () => {}); } catch {}
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
settleLoaded(true);
|
|
221
|
+
if (onProfileMessage) child.channel?.unref();
|
|
222
|
+
else {
|
|
223
|
+
try { child.disconnect(); } catch {}
|
|
224
|
+
}
|
|
210
225
|
});
|
|
211
226
|
child.once('error', (error) => settleCompleted({ error }));
|
|
212
227
|
child.once('exit', (code, signal) => settleCompleted({ code, signal }));
|
|
213
|
-
return { child, completed, loaded };
|
|
228
|
+
return { child, completed, loaded, isProfileReady: () => profileReady };
|
|
214
229
|
}
|
|
215
230
|
|
|
216
231
|
async function runInformationalCore(args, env, cwd, releaseNotice) {
|
|
@@ -222,10 +237,14 @@ async function runInformationalCore(args, env, cwd, releaseNotice) {
|
|
|
222
237
|
return Number.isInteger(result.code) ? result.code : exitCodeForChild(result, false);
|
|
223
238
|
}
|
|
224
239
|
|
|
225
|
-
async function superviseProtectedCore(args, env, cwd, releaseNotice) {
|
|
226
|
-
let noticeReleased = false;
|
|
227
|
-
|
|
228
|
-
|
|
240
|
+
async function superviseProtectedCore(args, env, cwd, releaseNotice, options = {}) {
|
|
241
|
+
let noticeReleased = false;
|
|
242
|
+
let rollback;
|
|
243
|
+
let recoveryAttempted = false;
|
|
244
|
+
for (;;) {
|
|
245
|
+
const handoff = options.prepareProfile
|
|
246
|
+
? createProfileHandoff(env, target => runningTuiPid(target) !== null) : undefined;
|
|
247
|
+
const core = spawnProtectedCore(args, env, cwd, handoff && ((message) => handoff.prepare(message)));
|
|
229
248
|
const loaded = await core.loaded;
|
|
230
249
|
if (!noticeReleased) {
|
|
231
250
|
noticeReleased = true;
|
|
@@ -236,12 +255,47 @@ async function superviseProtectedCore(args, env, cwd, releaseNotice) {
|
|
|
236
255
|
const intentionalStop = consumeIntentionalStopMarker({
|
|
237
256
|
homeDir: env.BLUN_HOME, childPid: core.child.pid, nowMs: Date.now(),
|
|
238
257
|
});
|
|
239
|
-
recordRuntimeExit({
|
|
258
|
+
recordRuntimeExit({
|
|
240
259
|
homeDir: env.BLUN_HOME, source: 'launcher', kind: result.error ? 'child-error' : 'child-exit',
|
|
241
260
|
exitCode, signal: result.signal, phase: loaded ? 'runtime' : 'startup',
|
|
242
261
|
cliVersion: env.BLUN_PUBLIC_PACKAGE_VERSION, profile: env.BLUN_PROFILE,
|
|
243
|
-
loaded, childPid: core.child.pid, error: result.error,
|
|
244
|
-
});
|
|
262
|
+
loaded, childPid: core.child.pid, error: result.error,
|
|
263
|
+
});
|
|
264
|
+
// A profile transition is committed only after the old process exits cleanly.
|
|
265
|
+
// Failed target startup restores the previous owner, never the previous PID.
|
|
266
|
+
if (rollback && (!loaded || !core.isProfileReady()) && !result.signal) {
|
|
267
|
+
({ args, env } = rollback);
|
|
268
|
+
rollback = undefined;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (loaded && core.isProfileReady()) {
|
|
272
|
+
rollback = undefined;
|
|
273
|
+
recoveryAttempted = false;
|
|
274
|
+
}
|
|
275
|
+
if (handoff && loaded && !intentionalStop && !result.error && !result.signal
|
|
276
|
+
&& result.code === PROFILE_HANDOFF_EXIT) {
|
|
277
|
+
if (!handoff.hasPending()) return 1;
|
|
278
|
+
const sourceSessionId = handoff.getSourceSessionId();
|
|
279
|
+
const recoveryArgs = sourceSessionId === undefined ? ['-c'] : ['-r', sourceSessionId];
|
|
280
|
+
try {
|
|
281
|
+
const selected = handoff.commit();
|
|
282
|
+
const nextEnv = await options.prepareProfile(selected);
|
|
283
|
+
handoff.commit();
|
|
284
|
+
rollback = { args: recoveryArgs, env };
|
|
285
|
+
args = ['-c'];
|
|
286
|
+
env = { ...nextEnv, BLUN_PROFILE_HANDOFF: '1' };
|
|
287
|
+
continue;
|
|
288
|
+
} catch (error) {
|
|
289
|
+
recordRuntimeExit({ homeDir: env.BLUN_HOME, source: 'launcher', kind: 'profile-handoff-failure',
|
|
290
|
+
phase: 'startup', cliVersion: env.BLUN_PUBLIC_PACKAGE_VERSION, profile: env.BLUN_PROFILE,
|
|
291
|
+
exitCode: 1, error });
|
|
292
|
+
if (recoveryAttempted) return 1;
|
|
293
|
+
recoveryAttempted = true;
|
|
294
|
+
// Reopen the original owner when the selection became unavailable.
|
|
295
|
+
args = recoveryArgs;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
245
299
|
if (intentionalStop || !isAbnormalCoreExit(result, loaded)) {
|
|
246
300
|
if (result.error) throw result.error;
|
|
247
301
|
return exitCode;
|
|
@@ -272,7 +326,7 @@ function exitCodeForChild(result, loaded) {
|
|
|
272
326
|
return 1;
|
|
273
327
|
}
|
|
274
328
|
|
|
275
|
-
function installTelegramForProfile(packageRoot, blunDir) {
|
|
329
|
+
function installTelegramForProfile(packageRoot, blunDir) {
|
|
276
330
|
installManagedTelegramPlugin({ packageRoot, blunDir });
|
|
277
331
|
const channelsDir = path.join(blunDir, 'channels');
|
|
278
332
|
const channelDir = path.join(channelsDir, 'telegram');
|
|
@@ -284,7 +338,37 @@ function installTelegramForProfile(packageRoot, blunDir) {
|
|
|
284
338
|
} else {
|
|
285
339
|
securePrivateFile(envFile);
|
|
286
340
|
}
|
|
287
|
-
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function prepareProfileRuntime(blunDir, callerCwd, mode) {
|
|
344
|
+
ensurePrivateDirectory(blunDir);
|
|
345
|
+
const cfgFile = path.join(blunDir, 'config.toml');
|
|
346
|
+
if (fs.existsSync(cfgFile)) securePrivateFile(cfgFile);
|
|
347
|
+
const standardMcpNames = availableStandardToolNames({ packageRoot: PKG })
|
|
348
|
+
.filter((name) => name !== 'telegram');
|
|
349
|
+
seedStandardTools({ packageRoot: PKG, blunDir, names: standardMcpNames });
|
|
350
|
+
seedStandardDesignSkill({ packageRoot: PKG, blunDir });
|
|
351
|
+
installManagedAgentSpinePlugin({ packageRoot: PKG, blunDir });
|
|
352
|
+
if (mode === LAUNCHER_MODES.KING) installTelegramForProfile(PKG, blunDir);
|
|
353
|
+
return repairConfiguredNativeModules({
|
|
354
|
+
blunDir, callerCwd,
|
|
355
|
+
onRepairStart() {
|
|
356
|
+
process.stdout.write('BLUN richtet native Node.js-Module fuer die neue Laufzeit ein.\n');
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function withProfileRuntimeReservation(homeDir, work) {
|
|
362
|
+
const result = await tryAcquireUpdateLease({ packageRoot: homeDir, role: 'profile' });
|
|
363
|
+
if (!result.acquired) throw Object.assign(new Error(`PROFILE_RUNTIME_${result.reason.toUpperCase()}`),
|
|
364
|
+
{ code: result.reason === 'busy' ? 'PROFILE_RUNTIME_BUSY' : 'PROFILE_RUNTIME_UNAVAILABLE' });
|
|
365
|
+
try {
|
|
366
|
+
result.lease.assertOwned();
|
|
367
|
+
const value = await work();
|
|
368
|
+
result.lease.assertOwned();
|
|
369
|
+
return value;
|
|
370
|
+
} finally { await result.lease.release(); }
|
|
371
|
+
}
|
|
288
372
|
|
|
289
373
|
function spawnManagedLauncher(binary, cwd) {
|
|
290
374
|
return new Promise((resolve, reject) => {
|
|
@@ -511,51 +595,42 @@ async function runLauncher(options = {}) {
|
|
|
511
595
|
// --- 2. Bestehende Konfiguration absichern -------------------------------
|
|
512
596
|
// Ein frischer Start bleibt absichtlich ohne statischen Anbieter. /login
|
|
513
597
|
// provisioniert King ueber den BLUN-OAuth-Server.
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
598
|
+
require('./verify-bundled-agent-sources.cjs').verifyBundledAgentSources(PKG);
|
|
599
|
+
const nativeModules = mode === LAUNCHER_MODES.KING && isInteractiveKingStart(ARGS)
|
|
600
|
+
? await withProfileRuntimeReservation(blunDir, () => prepareProfileRuntime(blunDir, callerCwd, mode))
|
|
601
|
+
: prepareProfileRuntime(blunDir, callerCwd, mode);
|
|
517
602
|
|
|
518
|
-
// --- 3. BLUN-Standard-MCPs additiv im Benutzerprofil einrichten ---------
|
|
519
|
-
const standardMcpNames = availableStandardToolNames({ packageRoot: PKG })
|
|
520
|
-
.filter((name) => name !== 'telegram');
|
|
521
|
-
seedStandardTools({
|
|
522
|
-
packageRoot: PKG,
|
|
523
|
-
blunDir,
|
|
524
|
-
names: standardMcpNames,
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
// --- 4. Eigenes BLUN-Grunddesign als einzigen Standard-Skill installieren
|
|
528
|
-
seedStandardDesignSkill({ packageRoot: PKG, blunDir });
|
|
529
|
-
installManagedAgentSpinePlugin({ packageRoot: PKG, blunDir });
|
|
530
|
-
|
|
531
|
-
// --- 5. Telegram-Plugin + Token-Vorlage (nur King) -----------------------
|
|
532
|
-
if (mode === LAUNCHER_MODES.KING) {
|
|
533
|
-
installTelegramForProfile(PKG, blunDir);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
const nativeModules = repairConfiguredNativeModules({
|
|
537
|
-
blunDir,
|
|
538
|
-
callerCwd,
|
|
539
|
-
onRepairStart() {
|
|
540
|
-
process.stdout.write('BLUN richtet native Node.js-Module fuer die neue Laufzeit ein.\n');
|
|
541
|
-
},
|
|
542
|
-
});
|
|
543
603
|
if (nativeModules.repaired.length > 0) {
|
|
544
604
|
process.stdout.write('Native Node.js-Module sind wieder einsatzbereit.\n');
|
|
545
605
|
}
|
|
546
606
|
|
|
547
|
-
// --- 6. Start -----------------------------------------------------------
|
|
607
|
+
// --- 6. Start -----------------------------------------------------------
|
|
548
608
|
const env = createLauncherEnvironment(process.env, mode, readPackageVersion(), blunDir);
|
|
549
609
|
env.BLUN_HOME = blunDir;
|
|
550
610
|
if (PROFILE.name !== undefined) {
|
|
551
611
|
env.BLUN_SHARED_HOME = getPrivatePaths().sharedHome;
|
|
552
612
|
env.BLUN_PROFILE = PROFILE.name;
|
|
553
613
|
}
|
|
614
|
+
const handoffOptions = mode === LAUNCHER_MODES.KING && isInteractiveKingStart(ARGS)
|
|
615
|
+
? { prepareProfile: async (selected) => {
|
|
616
|
+
const target = selected.homeDir;
|
|
617
|
+
await withProfileRuntimeReservation(target, () => prepareProfileRuntime(target, callerCwd, mode));
|
|
618
|
+
const next = createLauncherEnvironment(process.env, mode, readPackageVersion(), target);
|
|
619
|
+
next.BLUN_HOME = target;
|
|
620
|
+
next.BLUN_SHARED_HOME = selected.sharedHome;
|
|
621
|
+
next.BLUN_PROFILE = selected.name;
|
|
622
|
+
delete next.BLUN_TELEGRAM_STATE_DIR;
|
|
623
|
+
delete next.BLUN_PLUGIN_ROOT;
|
|
624
|
+
return next;
|
|
625
|
+
} } : {};
|
|
626
|
+
if (handoffOptions.prepareProfile) env.BLUN_PROFILE_HANDOFF = '1';
|
|
627
|
+
else delete env.BLUN_PROFILE_HANDOFF;
|
|
554
628
|
process.exitCode = await superviseProtectedCore(
|
|
555
629
|
ARGS,
|
|
556
630
|
env,
|
|
557
631
|
callerCwd,
|
|
558
|
-
releaseNotice,
|
|
632
|
+
releaseNotice,
|
|
633
|
+
handoffOptions,
|
|
559
634
|
);
|
|
560
635
|
} finally {
|
|
561
636
|
await releaseNotice();
|
|
@@ -563,6 +638,7 @@ async function runLauncher(options = {}) {
|
|
|
563
638
|
}
|
|
564
639
|
|
|
565
640
|
module.exports = {
|
|
641
|
+
withProfileRuntimeReservation,
|
|
566
642
|
resolveLauncherPrivatePaths,
|
|
567
643
|
isInteractiveKingStart,
|
|
568
644
|
runningTuiPid,
|
|
@@ -5,8 +5,10 @@ const path = require('node:path');
|
|
|
5
5
|
const { ensurePrivateDirectory, securePrivateTree } = require('./private-paths');
|
|
6
6
|
const SCHEMA = 'blun.managed-plugin-selection/v1';
|
|
7
7
|
|
|
8
|
-
function snapshotPluginTree(root) {
|
|
9
|
-
const entries = [];
|
|
8
|
+
function snapshotPluginTree(root, expectedEntries = null) {
|
|
9
|
+
const entries = [];
|
|
10
|
+
const expectedByPath = expectedEntries === null ? null
|
|
11
|
+
: new Map(expectedEntries.map(entry => [entry.path, entry]));
|
|
10
12
|
const rootStat = fs.lstatSync(root);
|
|
11
13
|
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) return null;
|
|
12
14
|
const visit = (directory, relativeDirectory = '') => {
|
|
@@ -14,19 +16,24 @@ function snapshotPluginTree(root) {
|
|
|
14
16
|
.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0)) {
|
|
15
17
|
const relativePath = path.join(relativeDirectory, entry.name);
|
|
16
18
|
const absolutePath = path.join(directory, entry.name);
|
|
17
|
-
const stat = fs.lstatSync(absolutePath);
|
|
18
|
-
if (stat.isSymbolicLink()) return false;
|
|
19
|
-
|
|
19
|
+
const stat = fs.lstatSync(absolutePath);
|
|
20
|
+
if (stat.isSymbolicLink()) return false;
|
|
21
|
+
const expected = expectedByPath?.get(relativePath);
|
|
22
|
+
if (expectedByPath && !expected) return false;
|
|
23
|
+
if (stat.isDirectory()) {
|
|
24
|
+
if (expected && expected.kind !== 'directory') return false;
|
|
20
25
|
entries.push({ kind: 'directory', path: relativePath });
|
|
21
26
|
if (!visit(absolutePath, relativePath)) return false;
|
|
22
|
-
} else if (stat.isFile()) {
|
|
27
|
+
} else if (stat.isFile()) {
|
|
28
|
+
if (expected && (expected.kind !== 'file' || stat.size !== expected.content.length)) return false;
|
|
23
29
|
entries.push({ kind: 'file', path: relativePath, content: fs.readFileSync(absolutePath) });
|
|
24
30
|
} else return false;
|
|
25
31
|
}
|
|
26
32
|
return true;
|
|
27
33
|
};
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
if (!visit(root) || (expectedByPath && entries.length !== expectedByPath.size)) return null;
|
|
35
|
+
return entries;
|
|
36
|
+
}
|
|
30
37
|
|
|
31
38
|
function digestPluginEntries(entries) {
|
|
32
39
|
if (entries === null) throw new Error('Invalid managed plugin tree');
|
package/bin/plugin-bootstrap.js
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
const { randomUUID } = require('node:crypto');
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const path = require('node:path');
|
|
6
|
-
const {
|
|
6
|
+
const {
|
|
7
|
+
selectedAgentSpineRoot,
|
|
8
|
+
describeAgentSpineSelection,
|
|
9
|
+
copySelectedAgentSpinePlugin,
|
|
10
|
+
snapshotPluginTree,
|
|
11
|
+
digestPluginEntries,
|
|
12
|
+
} = require('./managed-plugin-selection.cjs');
|
|
7
13
|
|
|
8
14
|
const {
|
|
9
15
|
ensurePrivateDirectory,
|
|
@@ -88,20 +94,37 @@ function installManagedTelegramPlugin({ packageRoot, blunDir, now = new Date().t
|
|
|
88
94
|
|
|
89
95
|
ensurePrivateDirectory(pluginsDir);
|
|
90
96
|
if (fs.existsSync(installedFile)) securePrivateFile(installedFile);
|
|
91
|
-
const installed = readInstalledPlugins(installedFile);
|
|
92
|
-
const updatedInstalled = updatedInstalledPlugins(installed, pluginRoot, now);
|
|
93
|
-
ensurePrivateDirectory(managedDir);
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
97
|
+
const installed = readInstalledPlugins(installedFile);
|
|
98
|
+
const updatedInstalled = updatedInstalledPlugins(installed, pluginRoot, now);
|
|
99
|
+
ensurePrivateDirectory(managedDir);
|
|
100
|
+
const expectedEntries = snapshotPluginTree(sourceRoot);
|
|
101
|
+
const expectedDigest = digestPluginEntries(expectedEntries);
|
|
102
|
+
let hadPreviousPlugin = false;
|
|
103
|
+
if (fs.existsSync(pluginRoot)) {
|
|
104
|
+
ensurePrivateDirectory(pluginRoot);
|
|
105
|
+
hadPreviousPlugin = true;
|
|
106
|
+
const existingEntries = snapshotPluginTree(pluginRoot, expectedEntries);
|
|
107
|
+
if (existingEntries !== null && digestPluginEntries(existingEntries) === expectedDigest) {
|
|
108
|
+
const previous = installed.plugins.find((plugin) => plugin.id === 'telegram');
|
|
109
|
+
const previousUpdate = typeof previous?.updatedAt === 'string' && previous.updatedAt.length > 0
|
|
110
|
+
? previous.updatedAt : now;
|
|
111
|
+
const reusedInstalled = updatedInstalledPlugins(installed, pluginRoot, previousUpdate);
|
|
112
|
+
if (JSON.stringify(reusedInstalled) !== JSON.stringify(installed)) {
|
|
113
|
+
writeInstalledPlugins(installedFile, reusedInstalled);
|
|
114
|
+
}
|
|
115
|
+
return { installedFile, pluginRoot, reused: true };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const nonce = `${process.pid}-${randomUUID()}`;
|
|
119
|
+
const stagingRoot = path.join(managedDir, `.telegram-${nonce}.tmp`);
|
|
120
|
+
const backupRoot = path.join(managedDir, `.telegram-${nonce}.bak`);
|
|
121
|
+
let installedNewPlugin = false;
|
|
122
|
+
try {
|
|
123
|
+
fs.cpSync(sourceRoot, stagingRoot, { recursive: true, force: true });
|
|
124
|
+
const stagedEntries = snapshotPluginTree(stagingRoot, expectedEntries);
|
|
125
|
+
if (stagedEntries === null || digestPluginEntries(stagedEntries) !== expectedDigest) {
|
|
126
|
+
throw new Error('Telegram source changed during installation');
|
|
127
|
+
}
|
|
105
128
|
if (hadPreviousPlugin) fs.renameSync(pluginRoot, backupRoot);
|
|
106
129
|
try {
|
|
107
130
|
fs.renameSync(stagingRoot, pluginRoot);
|
|
@@ -119,8 +142,8 @@ function installManagedTelegramPlugin({ packageRoot, blunDir, now = new Date().t
|
|
|
119
142
|
fs.rmSync(stagingRoot, { recursive: true, force: true });
|
|
120
143
|
}
|
|
121
144
|
|
|
122
|
-
return { installedFile, pluginRoot };
|
|
123
|
-
}
|
|
145
|
+
return { installedFile, pluginRoot, reused: false };
|
|
146
|
+
}
|
|
124
147
|
|
|
125
148
|
function installManagedAgentSpinePlugin({ packageRoot, blunDir, now = new Date().toISOString() }) {
|
|
126
149
|
const installedFile = path.join(blunDir, 'plugins', 'installed.json');
|
package/bin/private-paths.js
CHANGED
|
@@ -667,8 +667,16 @@ public static class BlunPrivateAclNative
|
|
|
667
667
|
VerifyHandlePath(handle, fullTarget);
|
|
668
668
|
HashSet<string> expectedSids = ExpectedSids(userSid);
|
|
669
669
|
string owner = ValidateOwner(handle, expectedSids);
|
|
670
|
-
|
|
671
|
-
|
|
670
|
+
// Avoid rewriting an exact directory DACL on every launcher start.
|
|
671
|
+
try
|
|
672
|
+
{
|
|
673
|
+
VerifyExactDacl(handle, userSid, true, owner);
|
|
674
|
+
}
|
|
675
|
+
catch (UnauthorizedAccessException)
|
|
676
|
+
{
|
|
677
|
+
SetExactDacl(handle, userSid, true);
|
|
678
|
+
VerifyExactDacl(handle, userSid, true, owner);
|
|
679
|
+
}
|
|
672
680
|
VerifyHandlePath(handle, fullTarget);
|
|
673
681
|
}
|
|
674
682
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { randomUUID } = require('node:crypto');
|
|
6
|
+
const { listExistingProfiles, resolveSelectedProfile } = require('./profile-launch.cjs');
|
|
7
|
+
|
|
8
|
+
const PROFILE_HANDOFF_REQUEST = 'blun-profile-handoff-request';
|
|
9
|
+
const PROFILE_HANDOFF_REPLY = 'blun-profile-handoff-reply';
|
|
10
|
+
const PROFILE_HANDOFF_CANCEL = 'blun-profile-handoff-cancel';
|
|
11
|
+
const PROFILE_HANDOFF_READY = 'blun-profile-handoff-ready';
|
|
12
|
+
const PROFILE_HANDOFF_EXIT = 76;
|
|
13
|
+
const REQUEST_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
14
|
+
const SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/u;
|
|
15
|
+
const samePath = (a, b) => process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
16
|
+
const unavailable = () => Object.assign(new Error('PROFILE_HANDOFF_UNAVAILABLE'), { code: 'PROFILE_HANDOFF_UNAVAILABLE' });
|
|
17
|
+
|
|
18
|
+
function createProfileHandoff(env, isBusy = () => false) {
|
|
19
|
+
let pending;
|
|
20
|
+
const validate = (name) => {
|
|
21
|
+
if (typeof env.BLUN_HOME !== 'string' || !path.isAbsolute(env.BLUN_HOME)
|
|
22
|
+
|| (env.BLUN_SHARED_HOME !== undefined && !path.isAbsolute(env.BLUN_SHARED_HOME))) throw unavailable();
|
|
23
|
+
const sharedHome = path.resolve(env.BLUN_SHARED_HOME || env.BLUN_HOME);
|
|
24
|
+
const homeDir = path.resolve(env.BLUN_HOME);
|
|
25
|
+
const current = env.BLUN_PROFILE;
|
|
26
|
+
if (current !== undefined) {
|
|
27
|
+
if (!env.BLUN_SHARED_HOME || path.basename(resolveSelectedProfile(sharedHome, current)) !== current
|
|
28
|
+
|| !samePath(homeDir, resolveSelectedProfile(sharedHome, current))) throw unavailable();
|
|
29
|
+
} else if (!samePath(homeDir, sharedHome)) throw unavailable();
|
|
30
|
+
const names = listExistingProfiles(sharedHome);
|
|
31
|
+
if (typeof name !== 'string' || !names.includes(name) || name === current
|
|
32
|
+
|| (current !== undefined && !names.includes(current))) throw unavailable();
|
|
33
|
+
const target = resolveSelectedProfile(sharedHome, name);
|
|
34
|
+
if (isBusy(target)) throw unavailable();
|
|
35
|
+
const identities = [sharedHome, path.join(sharedHome, 'profile'), homeDir, target]
|
|
36
|
+
.map(file => ({ file, stat: fs.lstatSync(file, { bigint: true }) }));
|
|
37
|
+
if (identities.some(({ stat }) => !stat.isDirectory() || stat.isSymbolicLink())) throw unavailable();
|
|
38
|
+
return { name, sharedHome, homeDir: target, identities };
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
hasPending: () => pending !== undefined,
|
|
42
|
+
getSourceSessionId: () => pending?.sourceSessionId,
|
|
43
|
+
prepare(message) {
|
|
44
|
+
if (message?.type === PROFILE_HANDOFF_CANCEL && typeof message.requestId === 'string'
|
|
45
|
+
&& REQUEST_ID.test(message.requestId) && Object.keys(message).sort().join(',') === 'requestId,type') {
|
|
46
|
+
if (pending?.requestId === message.requestId) pending = undefined;
|
|
47
|
+
return { type: PROFILE_HANDOFF_REPLY, requestId: message.requestId, status: 'cancelled' };
|
|
48
|
+
}
|
|
49
|
+
if (!message || message.type !== PROFILE_HANDOFF_REQUEST || typeof message.requestId !== 'string' || !REQUEST_ID.test(message.requestId)) return undefined;
|
|
50
|
+
const shape = Object.keys(message).sort().join(',');
|
|
51
|
+
if (shape !== 'name,requestId,type' && shape !== 'name,requestId,sourceSessionId,type') return undefined;
|
|
52
|
+
if (shape === 'name,requestId,sourceSessionId,type'
|
|
53
|
+
&& (typeof message.sourceSessionId !== 'string' || !SESSION_ID.test(message.sourceSessionId))) return undefined;
|
|
54
|
+
let status = 'rejected';
|
|
55
|
+
try {
|
|
56
|
+
if (pending) {
|
|
57
|
+
if (pending.requestId === message.requestId && pending.name === message.name
|
|
58
|
+
&& pending.sourceSessionId === message.sourceSessionId) status = 'prepared';
|
|
59
|
+
} else {
|
|
60
|
+
pending = { ...validate(message.name), requestId: message.requestId, sourceSessionId: message.sourceSessionId };
|
|
61
|
+
status = 'prepared';
|
|
62
|
+
}
|
|
63
|
+
} catch {}
|
|
64
|
+
return { type: PROFILE_HANDOFF_REPLY, requestId: message.requestId, status };
|
|
65
|
+
},
|
|
66
|
+
commit() {
|
|
67
|
+
try {
|
|
68
|
+
if (!pending) throw unavailable();
|
|
69
|
+
const fresh = validate(pending.name);
|
|
70
|
+
for (let index = 0; index < pending.identities.length; index++) {
|
|
71
|
+
const before = pending.identities[index];
|
|
72
|
+
const after = fresh.identities[index];
|
|
73
|
+
if (before.file !== after.file || before.stat.dev !== after.stat.dev || before.stat.ino !== after.stat.ino) throw unavailable();
|
|
74
|
+
}
|
|
75
|
+
return Object.freeze({ name: fresh.name, sharedHome: fresh.sharedHome, homeDir: fresh.homeDir,
|
|
76
|
+
...(pending.sourceSessionId === undefined ? {} : { sourceSessionId: pending.sourceSessionId }) });
|
|
77
|
+
} catch {
|
|
78
|
+
throw unavailable();
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function createProfileHandoffClient(channel = process) {
|
|
85
|
+
const enabled = channel.env?.BLUN_PROFILE_HANDOFF === '1';
|
|
86
|
+
let pending;
|
|
87
|
+
let disposed = false;
|
|
88
|
+
const available = () => !disposed && enabled && channel.connected === true
|
|
89
|
+
&& typeof channel.send === 'function';
|
|
90
|
+
const cancel = () => {
|
|
91
|
+
const request = pending;
|
|
92
|
+
if (!request) return;
|
|
93
|
+
pending = undefined;
|
|
94
|
+
request.finish(false);
|
|
95
|
+
if (channel.connected === true) {
|
|
96
|
+
try { channel.send({ type: PROFILE_HANDOFF_CANCEL, requestId: request.id }, () => {}); } catch {}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
return Object.freeze({
|
|
100
|
+
isPrepared: () => available() && pending?.prepared === true,
|
|
101
|
+
cancel,
|
|
102
|
+
dispose() { cancel(); disposed = true; },
|
|
103
|
+
/** @param {string} name @param {{ signal?: AbortSignal, timeoutMs?: number, sourceSessionId?: string }} [options] */
|
|
104
|
+
prepare(name, { signal, timeoutMs = 5000, sourceSessionId } = {}) {
|
|
105
|
+
try {
|
|
106
|
+
if (!available() || pending || signal?.aborted || typeof name !== 'string'
|
|
107
|
+
|| path.basename(resolveSelectedProfile(path.parse(__dirname).root, name)) !== name
|
|
108
|
+
|| !Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30000
|
|
109
|
+
|| (sourceSessionId !== undefined && (typeof sourceSessionId !== 'string' || !SESSION_ID.test(sourceSessionId)))) return Promise.resolve(false);
|
|
110
|
+
} catch { return Promise.resolve(false); }
|
|
111
|
+
return new Promise(resolve => {
|
|
112
|
+
const request = { id: randomUUID(), prepared: false, finish: undefined };
|
|
113
|
+
let settled = false;
|
|
114
|
+
let timer;
|
|
115
|
+
const aborted = () => cancel();
|
|
116
|
+
const disconnected = () => cancel();
|
|
117
|
+
const message = reply => {
|
|
118
|
+
if (pending !== request || reply?.type !== PROFILE_HANDOFF_REPLY || reply.requestId !== request.id
|
|
119
|
+
|| Object.keys(reply).sort().join(',') !== 'requestId,status,type') return;
|
|
120
|
+
if (reply.status === 'prepared') request.finish(true);
|
|
121
|
+
else if (reply.status === 'rejected' || reply.status === 'cancelled') cancel();
|
|
122
|
+
};
|
|
123
|
+
request.finish = prepared => {
|
|
124
|
+
if (!prepared) signal?.removeEventListener('abort', aborted);
|
|
125
|
+
if (settled) return;
|
|
126
|
+
settled = true;
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
channel.removeListener('message', message);
|
|
129
|
+
channel.removeListener('disconnect', disconnected);
|
|
130
|
+
try { channel.channel?.unref(); } catch {}
|
|
131
|
+
request.prepared = prepared;
|
|
132
|
+
resolve(prepared);
|
|
133
|
+
};
|
|
134
|
+
pending = request;
|
|
135
|
+
channel.on('message', message);
|
|
136
|
+
channel.once('disconnect', disconnected);
|
|
137
|
+
signal?.addEventListener('abort', aborted, { once: true });
|
|
138
|
+
timer = setTimeout(aborted, timeoutMs);
|
|
139
|
+
try {
|
|
140
|
+
channel.channel?.ref();
|
|
141
|
+
channel.send({ type: PROFILE_HANDOFF_REQUEST, requestId: request.id, name,
|
|
142
|
+
...(sourceSessionId === undefined ? {} : { sourceSessionId }) }, error => {
|
|
143
|
+
if (error && pending === request) cancel();
|
|
144
|
+
});
|
|
145
|
+
} catch { cancel(); }
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = { createProfileHandoff, createProfileHandoffClient, PROFILE_HANDOFF_CANCEL, PROFILE_HANDOFF_REQUEST, PROFILE_HANDOFF_REPLY,
|
|
152
|
+
PROFILE_HANDOFF_READY, PROFILE_HANDOFF_EXIT };
|
package/bin/profile-launch.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const path = require('node:path');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const fs = require('node:fs');
|
|
4
5
|
|
|
5
6
|
const PROFILE_NAME = /^[a-z0-9][a-z0-9_-]{0,31}$/u;
|
|
6
7
|
const RESERVED = new Set(['doctor', 'export', 'help', 'login', 'mcp', 'server', 'tools', 'update', 'upgrade', 'version']);
|
|
@@ -35,8 +36,85 @@ function parseProfileLaunchArgs(args, mode) {
|
|
|
35
36
|
return { args: remaining, name: undefined };
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
function resolveSelectedProfile(sharedHome, name, pathImpl = path) {
|
|
39
|
-
return pathImpl.join(sharedHome, 'profile', profileName(name));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
function resolveSelectedProfile(sharedHome, name, pathImpl = path) {
|
|
40
|
+
return pathImpl.join(sharedHome, 'profile', profileName(name));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function catalogUnavailable() {
|
|
44
|
+
return Object.assign(new Error('PROFILE_CATALOG_UNAVAILABLE'), { code: 'PROFILE_CATALOG_UNAVAILABLE' });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function directoryIdentity(target, fsImpl) {
|
|
48
|
+
let stat;
|
|
49
|
+
try { stat = fsImpl.lstatSync(target, { bigint: true }); }
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error.code === 'ENOENT') return undefined;
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw catalogUnavailable();
|
|
55
|
+
return stat;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function homeIdentity(home, fsImpl) {
|
|
59
|
+
const root = path.parse(home).root;
|
|
60
|
+
let current = root;
|
|
61
|
+
let stat = directoryIdentity(current, fsImpl);
|
|
62
|
+
for (const part of home.slice(root.length).split(path.sep).filter(Boolean)) {
|
|
63
|
+
if (stat === undefined) return undefined;
|
|
64
|
+
current = path.join(current, part);
|
|
65
|
+
stat = directoryIdentity(current, fsImpl);
|
|
66
|
+
}
|
|
67
|
+
return stat;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sameDirectory(before, after) {
|
|
71
|
+
return before !== undefined && after !== undefined && before.dev === after.dev && before.ino === after.ino;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Directory names only; selection must revalidate them before changing owner. */
|
|
75
|
+
function listExistingProfiles(sharedHome, options = {}) {
|
|
76
|
+
const fsImpl = options.fsImpl || fs;
|
|
77
|
+
try {
|
|
78
|
+
if (typeof sharedHome !== 'string' || !path.isAbsolute(sharedHome)) throw catalogUnavailable();
|
|
79
|
+
const home = path.resolve(sharedHome);
|
|
80
|
+
if (process.platform === 'win32' && home.startsWith('\\\\')) throw catalogUnavailable();
|
|
81
|
+
const beforeHome = homeIdentity(home, fsImpl);
|
|
82
|
+
if (beforeHome === undefined) return [];
|
|
83
|
+
const profileRoot = path.join(home, 'profile');
|
|
84
|
+
const beforeProfiles = directoryIdentity(profileRoot, fsImpl);
|
|
85
|
+
if (beforeProfiles === undefined) {
|
|
86
|
+
if (!sameDirectory(beforeHome, homeIdentity(home, fsImpl))) throw catalogUnavailable();
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
const rows = [];
|
|
90
|
+
const dir = fsImpl.opendirSync(profileRoot);
|
|
91
|
+
try {
|
|
92
|
+
let count = 0;
|
|
93
|
+
for (let entry; (entry = dir.readSync()) !== null;) {
|
|
94
|
+
if (++count > 4096) throw catalogUnavailable();
|
|
95
|
+
const name = entry.name;
|
|
96
|
+
if (!PROFILE_NAME.test(name)) continue;
|
|
97
|
+
const target = resolveSelectedProfile(home, name);
|
|
98
|
+
let stat;
|
|
99
|
+
try { stat = fsImpl.lstatSync(target, { bigint: true }); }
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (error.code === 'ENOENT') continue;
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
if (stat.isDirectory() && !stat.isSymbolicLink()) rows.push({ name, target, stat });
|
|
105
|
+
}
|
|
106
|
+
} finally { dir.closeSync(); }
|
|
107
|
+
if (!sameDirectory(beforeHome, homeIdentity(home, fsImpl))
|
|
108
|
+
|| !sameDirectory(beforeProfiles, directoryIdentity(profileRoot, fsImpl))) throw catalogUnavailable();
|
|
109
|
+
for (const row of rows) {
|
|
110
|
+
if (!sameDirectory(row.stat, directoryIdentity(row.target, fsImpl))) throw catalogUnavailable();
|
|
111
|
+
}
|
|
112
|
+
if (!sameDirectory(beforeHome, homeIdentity(home, fsImpl))
|
|
113
|
+
|| !sameDirectory(beforeProfiles, directoryIdentity(profileRoot, fsImpl))) throw catalogUnavailable();
|
|
114
|
+
return rows.map(row => row.name).sort();
|
|
115
|
+
} catch {
|
|
116
|
+
throw catalogUnavailable();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = { parseProfileLaunchArgs, resolveSelectedProfile, listExistingProfiles };
|
package/bin/update-lease.js
CHANGED
|
@@ -34,7 +34,9 @@ function canonicalPackageIdentity(packageRoot, options = {}) {
|
|
|
34
34
|
|
|
35
35
|
function deriveUpdateLeaseEndpoint(packageRoot, options = {}) {
|
|
36
36
|
const platform = options.platform || process.platform;
|
|
37
|
-
const role = options.role === '
|
|
37
|
+
const role = options.role === 'profile'
|
|
38
|
+
? 'profile'
|
|
39
|
+
: options.role === 'install'
|
|
38
40
|
? 'install'
|
|
39
41
|
: options.role === 'runtime'
|
|
40
42
|
? 'runtime'
|