fraim-hub 2.0.310 → 2.0.312
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/dist/src/ai-hub/desktop-main.js +101 -5
- package/dist/src/ai-hub/devtools-window.js +17 -0
- package/dist/src/ai-hub/hosts.js +71 -12
- package/dist/src/ai-hub/hub-app-materializer.js +33 -3
- package/dist/src/ai-hub/hub-main-diagnostics.js +278 -0
- package/dist/src/ai-hub/restart-recovery-policy.js +11 -3
- package/dist/src/ai-hub/server.js +68 -16
- package/dist/src/cli/setup/ide-invocation-surfaces.js +1 -1
- package/dist/src/cli/setup/user-level-sync.js +4 -0
- package/dist/src/config/persona-capability-bundles.js +12 -1
- package/package.json +2 -2
- package/public/ai-hub/index.html +1 -1
- package/public/ai-hub/script.js +482 -120
- package/public/ai-hub/styles.css +29 -0
|
@@ -24,6 +24,8 @@ const fraim_mcp_latest_launcher_1 = require("../cli/mcp/fraim-mcp-latest-launche
|
|
|
24
24
|
const desktop_auto_updater_1 = require("./desktop-auto-updater");
|
|
25
25
|
const hub_app_materializer_1 = require("./hub-app-materializer");
|
|
26
26
|
const hub_instance_reconciliation_1 = require("./hub-instance-reconciliation");
|
|
27
|
+
const hub_main_diagnostics_1 = require("./hub-main-diagnostics");
|
|
28
|
+
const devtools_window_1 = require("./devtools-window");
|
|
27
29
|
// Keep installed, running, and user-pinned Windows shortcuts grouped under the
|
|
28
30
|
// stable identity declared in packages/fraim-hub/package.json.
|
|
29
31
|
electron_1.app.setAppUserModelId('ai.fraim.hub');
|
|
@@ -163,7 +165,7 @@ async function resolveLatestHubAppVersion() {
|
|
|
163
165
|
bundledVersion: runningVersion,
|
|
164
166
|
bundledEntryPath: __filename,
|
|
165
167
|
});
|
|
166
|
-
return { upgradable: resolved.version !== runningVersion, latest: resolved.version };
|
|
168
|
+
return { upgradable: resolved.version !== runningVersion, latest: resolved.version, error: resolved.materializationError };
|
|
167
169
|
}
|
|
168
170
|
// Issue #1415: a second-instance activation (the user re-clicking the icon while the Hub is
|
|
169
171
|
// already running) is the common case once login-item autostart + hide-to-tray keep the Hub
|
|
@@ -189,9 +191,12 @@ async function checkForHubAppUpdateOnSecondInstance() {
|
|
|
189
191
|
// response reaches the browser (which uses it to start polling for the new instance) before this
|
|
190
192
|
// process starts tearing down.
|
|
191
193
|
async function restartToLatestNow() {
|
|
192
|
-
const { upgradable, latest } = await resolveLatestHubAppVersion();
|
|
193
|
-
if (!upgradable)
|
|
194
|
-
|
|
194
|
+
const { upgradable, latest, error } = await resolveLatestHubAppVersion();
|
|
195
|
+
if (!upgradable) {
|
|
196
|
+
if (error)
|
|
197
|
+
console.warn(`[fraim] manual restart requested but could not complete: ${error}`);
|
|
198
|
+
return { restarting: false, latest, error };
|
|
199
|
+
}
|
|
195
200
|
console.log(`[fraim] manual restart requested; relaunching to FRAIM Hub ${latest}`);
|
|
196
201
|
setTimeout(() => {
|
|
197
202
|
electron_1.app.relaunch();
|
|
@@ -274,6 +279,14 @@ function shouldConfigureOfficeSideload() {
|
|
|
274
279
|
function displayName(runtimeId = process.env.FRAIM_HUB_RUNTIME_ID || 'hub') {
|
|
275
280
|
return process.env.FRAIM_HUB_DISPLAY_NAME || (runtimeId === 'hub2' ? 'FRAIM Hub 2' : 'FRAIM Hub');
|
|
276
281
|
}
|
|
282
|
+
async function openMainWindowDevTools(hubUrl) {
|
|
283
|
+
await (0, devtools_window_1.openHubWindowDevTools)({
|
|
284
|
+
hubUrl,
|
|
285
|
+
getWindow: () => mainWindow,
|
|
286
|
+
createWindow,
|
|
287
|
+
recordOpenRequested: () => (0, hub_main_diagnostics_1.recordHubMainDiagnostic)('hub.devtools.open_requested'),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
277
290
|
function buildTrayMenu(hubUrl, runtimeId) {
|
|
278
291
|
const name = displayName(runtimeId);
|
|
279
292
|
return electron_1.Menu.buildFromTemplate([
|
|
@@ -289,6 +302,14 @@ function buildTrayMenu(hubUrl, runtimeId) {
|
|
|
289
302
|
}
|
|
290
303
|
},
|
|
291
304
|
},
|
|
305
|
+
{
|
|
306
|
+
label: 'Open DevTools',
|
|
307
|
+
click: () => {
|
|
308
|
+
void openMainWindowDevTools(hubUrl).catch((error) => {
|
|
309
|
+
console.warn('[fraim] could not open Hub DevTools:', error);
|
|
310
|
+
});
|
|
311
|
+
},
|
|
312
|
+
},
|
|
292
313
|
{
|
|
293
314
|
// #755: surface the running build version so staleness is diagnosable at a glance.
|
|
294
315
|
label: `About ${name}`,
|
|
@@ -345,6 +366,36 @@ function createTray(hubUrl, runtimeId) {
|
|
|
345
366
|
// ---------------------------------------------------------------------------
|
|
346
367
|
// BrowserWindow
|
|
347
368
|
// ---------------------------------------------------------------------------
|
|
369
|
+
function buildDesktopDebugSnapshot(runtimeId) {
|
|
370
|
+
const win = mainWindow;
|
|
371
|
+
const windowPresent = Boolean(win);
|
|
372
|
+
const isDestroyed = win ? win.isDestroyed() : null;
|
|
373
|
+
const canReadWindow = Boolean(win && !isDestroyed);
|
|
374
|
+
const safeRead = (reader) => {
|
|
375
|
+
if (!canReadWindow)
|
|
376
|
+
return null;
|
|
377
|
+
try {
|
|
378
|
+
return reader();
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
return {
|
|
385
|
+
runtimeId,
|
|
386
|
+
windowPresent,
|
|
387
|
+
isDestroyed,
|
|
388
|
+
isFocused: safeRead(() => win.isFocused()),
|
|
389
|
+
isVisible: safeRead(() => win.isVisible()),
|
|
390
|
+
isMinimized: safeRead(() => win.isMinimized()),
|
|
391
|
+
bounds: safeRead(() => win.getBounds()),
|
|
392
|
+
title: safeRead(() => win.getTitle()),
|
|
393
|
+
url: safeRead(() => win.webContents.getURL()),
|
|
394
|
+
webContentsDestroyed: win ? safeRead(() => win.webContents.isDestroyed()) : null,
|
|
395
|
+
devToolsOpened: safeRead(() => win.webContents.isDevToolsOpened()),
|
|
396
|
+
gpuFeatureStatus: electron_1.app.getGPUFeatureStatus(),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
348
399
|
async function createWindow(url, runtimeId = process.env.FRAIM_HUB_RUNTIME_ID || 'hub') {
|
|
349
400
|
const { width, height } = preferredWindowSize();
|
|
350
401
|
const isMac = process.platform === 'darwin';
|
|
@@ -374,6 +425,32 @@ async function createWindow(url, runtimeId = process.env.FRAIM_HUB_RUNTIME_ID ||
|
|
|
374
425
|
webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true },
|
|
375
426
|
});
|
|
376
427
|
electron_1.Menu.setApplicationMenu(null);
|
|
428
|
+
mainWindow.on('focus', () => (0, hub_main_diagnostics_1.recordHubMainDiagnostic)('window.focus'));
|
|
429
|
+
mainWindow.on('blur', () => (0, hub_main_diagnostics_1.recordHubMainDiagnostic)('window.blur'));
|
|
430
|
+
mainWindow.on('show', () => (0, hub_main_diagnostics_1.recordHubMainDiagnostic)('window.show'));
|
|
431
|
+
mainWindow.on('hide', () => (0, hub_main_diagnostics_1.recordHubMainDiagnostic)('window.hide'));
|
|
432
|
+
mainWindow.on('close', () => (0, hub_main_diagnostics_1.recordHubMainDiagnostic)('window.close', { isQuitting }));
|
|
433
|
+
mainWindow.webContents.on('render-process-gone', (_event, details) => {
|
|
434
|
+
(0, hub_main_diagnostics_1.recordHubMainDiagnostic)('webcontents.render_process_gone', details);
|
|
435
|
+
});
|
|
436
|
+
mainWindow.webContents.on('unresponsive', () => (0, hub_main_diagnostics_1.recordHubMainDiagnostic)('webcontents.unresponsive'));
|
|
437
|
+
mainWindow.webContents.on('responsive', () => (0, hub_main_diagnostics_1.recordHubMainDiagnostic)('webcontents.responsive'));
|
|
438
|
+
mainWindow.webContents.on('did-finish-load', () => {
|
|
439
|
+
(0, hub_main_diagnostics_1.recordHubMainDiagnostic)('webcontents.did_finish_load', {
|
|
440
|
+
url: mainWindow?.webContents.getURL() || '',
|
|
441
|
+
title: mainWindow?.getTitle() || '',
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
mainWindow.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
|
|
445
|
+
(0, hub_main_diagnostics_1.recordHubMainDiagnostic)('webcontents.did_fail_load', {
|
|
446
|
+
errorCode,
|
|
447
|
+
errorDescription,
|
|
448
|
+
validatedURL,
|
|
449
|
+
isMainFrame,
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
mainWindow.webContents.on('devtools-opened', () => (0, hub_main_diagnostics_1.recordHubMainDiagnostic)('webcontents.devtools_opened'));
|
|
453
|
+
mainWindow.webContents.on('devtools-closed', () => (0, hub_main_diagnostics_1.recordHubMainDiagnostic)('webcontents.devtools_closed'));
|
|
377
454
|
try {
|
|
378
455
|
await mainWindow.webContents.session.clearCache();
|
|
379
456
|
}
|
|
@@ -448,7 +525,10 @@ async function createWindow(url, runtimeId = process.env.FRAIM_HUB_RUNTIME_ID ||
|
|
|
448
525
|
mainWindow?.hide();
|
|
449
526
|
}
|
|
450
527
|
});
|
|
451
|
-
mainWindow.on('closed', () => {
|
|
528
|
+
mainWindow.on('closed', () => {
|
|
529
|
+
(0, hub_main_diagnostics_1.recordHubMainDiagnostic)('window.closed');
|
|
530
|
+
mainWindow = null;
|
|
531
|
+
});
|
|
452
532
|
await mainWindow.loadURL(url);
|
|
453
533
|
mainWindow.webContents.setZoomFactor(1);
|
|
454
534
|
}
|
|
@@ -499,6 +579,9 @@ async function launchDesktopShell(options) {
|
|
|
499
579
|
return result.canceled || result.filePaths.length === 0 ? null : result.filePaths[0];
|
|
500
580
|
},
|
|
501
581
|
restartToLatest: restartToLatestNow,
|
|
582
|
+
hubMainDiagnostics: {
|
|
583
|
+
snapshot: () => (0, hub_main_diagnostics_1.getHubMainDiagnosticsSnapshot)(buildDesktopDebugSnapshot(runtimeId)),
|
|
584
|
+
},
|
|
502
585
|
});
|
|
503
586
|
await server.start(httpPort);
|
|
504
587
|
const resolvedProjectPath = server.getProjectPath();
|
|
@@ -566,6 +649,16 @@ async function launchDesktopShell(options) {
|
|
|
566
649
|
async function bootstrap() {
|
|
567
650
|
const options = parseArgs(process.argv.slice(2));
|
|
568
651
|
applyUserDataOverride();
|
|
652
|
+
(0, hub_main_diagnostics_1.installHubMainConsoleTee)();
|
|
653
|
+
(0, hub_main_diagnostics_1.recordHubMainDiagnostic)('hub.startup', {
|
|
654
|
+
runtimeId: options.runtimeId,
|
|
655
|
+
preferredPort: options.preferredPort,
|
|
656
|
+
hasProjectPath: Boolean(options.projectPath),
|
|
657
|
+
});
|
|
658
|
+
electron_1.app.on('child-process-gone', (_event, details) => {
|
|
659
|
+
(0, hub_main_diagnostics_1.recordHubMainDiagnostic)('app.child_process_gone', details);
|
|
660
|
+
});
|
|
661
|
+
electron_1.app.on('gpu-info-update', () => (0, hub_main_diagnostics_1.recordHubMainDiagnostic)('app.gpu_info_update'));
|
|
569
662
|
// Single-instance lock — if another instance is already running, focus it
|
|
570
663
|
// and exit rather than spawning a second server + window.
|
|
571
664
|
// Skip when FRAIM_AI_HUB_FAKE_HOST=1 (test mode) so Playwright can launch
|
|
@@ -577,6 +670,7 @@ async function bootstrap() {
|
|
|
577
670
|
return;
|
|
578
671
|
}
|
|
579
672
|
electron_1.app.on('second-instance', () => {
|
|
673
|
+
(0, hub_main_diagnostics_1.recordHubMainDiagnostic)('hub.second_instance');
|
|
580
674
|
void electron_1.app.whenReady().then(checkForRelaunchAttemptUpdate);
|
|
581
675
|
if (mainWindow) {
|
|
582
676
|
mainWindow.show();
|
|
@@ -624,6 +718,7 @@ async function bootstrap() {
|
|
|
624
718
|
// macOS: clicking dock icon re-shows the window. Unlike Windows/Linux, this never spawns a
|
|
625
719
|
// second process (so 'second-instance' never fires here) — it is macOS's equivalent
|
|
626
720
|
// relaunch-attempt event, so it gets the same npm-latest re-check (#1415).
|
|
721
|
+
(0, hub_main_diagnostics_1.recordHubMainDiagnostic)('hub.activate');
|
|
627
722
|
checkForRelaunchAttemptUpdate();
|
|
628
723
|
if (mainWindow) {
|
|
629
724
|
mainWindow.show();
|
|
@@ -632,6 +727,7 @@ async function bootstrap() {
|
|
|
632
727
|
});
|
|
633
728
|
electron_1.app.on('before-quit', () => {
|
|
634
729
|
isQuitting = true;
|
|
730
|
+
(0, hub_main_diagnostics_1.recordHubMainDiagnostic)('hub.shutdown');
|
|
635
731
|
// #755: clear the runtime file so a later `fraim hub` doesn't treat a
|
|
636
732
|
// cleanly-exited instance as live.
|
|
637
733
|
try {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.openHubWindowDevTools = openHubWindowDevTools;
|
|
4
|
+
async function openHubWindowDevTools(options) {
|
|
5
|
+
options.recordOpenRequested?.();
|
|
6
|
+
let window = options.getWindow();
|
|
7
|
+
if (!window || window.isDestroyed()) {
|
|
8
|
+
await options.createWindow(options.hubUrl);
|
|
9
|
+
window = options.getWindow();
|
|
10
|
+
}
|
|
11
|
+
if (!window || window.isDestroyed())
|
|
12
|
+
return 'unavailable';
|
|
13
|
+
window.show();
|
|
14
|
+
window.focus();
|
|
15
|
+
window.webContents.openDevTools({ mode: 'detach' });
|
|
16
|
+
return 'opened';
|
|
17
|
+
}
|
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -2501,15 +2501,13 @@ class CliHostRuntime {
|
|
|
2501
2501
|
return isEmployeeDetectionRefreshing();
|
|
2502
2502
|
}
|
|
2503
2503
|
startRun(hostId, projectPath, message, handlers, sessionId, launchContext) {
|
|
2504
|
-
|
|
2505
|
-
// stay outside the continuation queue entirely.
|
|
2506
|
-
return this.spawn(hostId, (0, configured_agents_1.decorateHostPlanWithConfiguredAgent)(buildStartPlan(hostId, message, sessionId), launchContext?.agent, launchContext?.env), projectPath, handlers);
|
|
2504
|
+
return this.spawnStartAndRegister(hostId, projectPath, handlers, sessionId, (0, configured_agents_1.decorateHostPlanWithConfiguredAgent)(buildStartPlan(hostId, message, sessionId), launchContext?.agent, launchContext?.env));
|
|
2507
2505
|
}
|
|
2508
2506
|
continueRun(hostId, projectPath, sessionId, message, handlers, launchContext, deliveryIntent) {
|
|
2509
2507
|
return this.guardedContinue(hostId, sessionId, { projectPath, message, handlers, launchContext, deliveryIntent });
|
|
2510
2508
|
}
|
|
2511
2509
|
startDirectRun(hostId, message, projectPath, handlers, sessionId, launchContext) {
|
|
2512
|
-
return this.
|
|
2510
|
+
return this.spawnStartAndRegister(hostId, projectPath, handlers, sessionId, (0, configured_agents_1.decorateHostPlanWithConfiguredAgent)(buildDirectStartPlan(hostId, message, sessionId), launchContext?.agent, launchContext?.env));
|
|
2513
2511
|
}
|
|
2514
2512
|
continueDirectRun(hostId, sessionId, message, projectPath, handlers, launchContext, deliveryIntent) {
|
|
2515
2513
|
// R10: continueDirectRun is protected by the same per-hostId::sessionId
|
|
@@ -2538,7 +2536,7 @@ class CliHostRuntime {
|
|
|
2538
2536
|
if (active.escalationTimer != null)
|
|
2539
2537
|
clearTimeout(active.escalationTimer);
|
|
2540
2538
|
active.pending.splice(0);
|
|
2541
|
-
this.
|
|
2539
|
+
this.clearActiveRun(active);
|
|
2542
2540
|
if (active.child.pid == null)
|
|
2543
2541
|
return false;
|
|
2544
2542
|
try {
|
|
@@ -2559,7 +2557,7 @@ class CliHostRuntime {
|
|
|
2559
2557
|
const key = `${hostId}::${sessionId}`;
|
|
2560
2558
|
const active = this.activeContinueRuns.get(key);
|
|
2561
2559
|
if (!active) {
|
|
2562
|
-
return this.spawnAndRegister(
|
|
2560
|
+
return this.spawnAndRegister(hostId, sessionId, entry);
|
|
2563
2561
|
}
|
|
2564
2562
|
// R3/R31: an ordinary follow-up queues behind existing work; a course
|
|
2565
2563
|
// correction (deliveryIntent 'stop') jumps to the front of the queue and
|
|
@@ -2579,11 +2577,49 @@ class CliHostRuntime {
|
|
|
2579
2577
|
// through `entry.handlers` once it is actually dequeued and spawned.
|
|
2580
2578
|
return active.child;
|
|
2581
2579
|
}
|
|
2582
|
-
|
|
2580
|
+
spawnStartAndRegister(hostId, projectPath, handlers, sessionId, plan) {
|
|
2581
|
+
const deferredSessionIds = [];
|
|
2582
|
+
let runEntry = null;
|
|
2583
|
+
const registerSessionId = (value) => {
|
|
2584
|
+
const discoveredSessionId = typeof value === 'string' ? value.trim() : '';
|
|
2585
|
+
if (!discoveredSessionId)
|
|
2586
|
+
return;
|
|
2587
|
+
if (!runEntry) {
|
|
2588
|
+
deferredSessionIds.push(discoveredSessionId);
|
|
2589
|
+
return;
|
|
2590
|
+
}
|
|
2591
|
+
this.registerActiveRun(hostId, discoveredSessionId, runEntry);
|
|
2592
|
+
};
|
|
2593
|
+
const wrappedHandlers = {
|
|
2594
|
+
...handlers,
|
|
2595
|
+
onEvent: (event, channel) => {
|
|
2596
|
+
registerSessionId(event.sessionId);
|
|
2597
|
+
handlers.onEvent(event, channel);
|
|
2598
|
+
},
|
|
2599
|
+
};
|
|
2600
|
+
const child = this.spawn(hostId, plan, projectPath, wrappedHandlers);
|
|
2601
|
+
runEntry = { child, pending: [], handlers: wrappedHandlers };
|
|
2602
|
+
if (sessionId)
|
|
2603
|
+
registerSessionId(sessionId);
|
|
2604
|
+
for (const discoveredSessionId of deferredSessionIds) {
|
|
2605
|
+
registerSessionId(discoveredSessionId);
|
|
2606
|
+
}
|
|
2607
|
+
child.once('close', () => {
|
|
2608
|
+
if (!runEntry)
|
|
2609
|
+
return;
|
|
2610
|
+
if (runEntry.escalationTimer != null) {
|
|
2611
|
+
clearTimeout(runEntry.escalationTimer);
|
|
2612
|
+
runEntry.escalationTimer = undefined;
|
|
2613
|
+
}
|
|
2614
|
+
this.dequeueNext(hostId, runEntry);
|
|
2615
|
+
});
|
|
2616
|
+
return child;
|
|
2617
|
+
}
|
|
2618
|
+
spawnAndRegister(hostId, sessionId, entry) {
|
|
2583
2619
|
const plan = (0, configured_agents_1.decorateHostPlanWithConfiguredAgent)(entry.direct ? buildDirectContinuePlan(hostId, sessionId, entry.message) : buildContinuePlan(hostId, sessionId, entry.message), entry.launchContext?.agent, entry.launchContext?.env);
|
|
2584
2620
|
const child = this.spawn(hostId, plan, entry.projectPath, entry.handlers);
|
|
2585
2621
|
const runEntry = { child, pending: [], handlers: entry.handlers };
|
|
2586
|
-
this.
|
|
2622
|
+
this.registerActiveRun(hostId, sessionId, runEntry);
|
|
2587
2623
|
child.once('close', () => {
|
|
2588
2624
|
// Issue #1570: a redirect kill that eventually takes (just slower than
|
|
2589
2625
|
// the escalation window) must not leave its retry timer dangling past
|
|
@@ -2592,10 +2628,30 @@ class CliHostRuntime {
|
|
|
2592
2628
|
clearTimeout(runEntry.escalationTimer);
|
|
2593
2629
|
runEntry.escalationTimer = undefined;
|
|
2594
2630
|
}
|
|
2595
|
-
this.dequeueNext(
|
|
2631
|
+
this.dequeueNext(hostId, runEntry);
|
|
2596
2632
|
});
|
|
2597
2633
|
return child;
|
|
2598
2634
|
}
|
|
2635
|
+
registerActiveRun(hostId, sessionId, runEntry) {
|
|
2636
|
+
const key = `${hostId}::${sessionId}`;
|
|
2637
|
+
const existing = this.activeContinueRuns.get(key);
|
|
2638
|
+
if (existing && existing !== runEntry)
|
|
2639
|
+
return;
|
|
2640
|
+
if (!runEntry.keys)
|
|
2641
|
+
runEntry.keys = new Set();
|
|
2642
|
+
runEntry.keys.add(key);
|
|
2643
|
+
runEntry.sessionId = sessionId;
|
|
2644
|
+
this.activeContinueRuns.set(key, runEntry);
|
|
2645
|
+
}
|
|
2646
|
+
clearActiveRun(runEntry) {
|
|
2647
|
+
const keys = runEntry.keys ? Array.from(runEntry.keys) : [];
|
|
2648
|
+
for (const key of keys) {
|
|
2649
|
+
if (this.activeContinueRuns.get(key) === runEntry) {
|
|
2650
|
+
this.activeContinueRuns.delete(key);
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
runEntry.keys?.clear();
|
|
2654
|
+
}
|
|
2599
2655
|
// Issue #1570 (Defect 1): the redirect kill was previously fire-and-forget
|
|
2600
2656
|
// — `killTree` was called once, with no way to know whether it actually
|
|
2601
2657
|
// took, and no ceiling on how long the queued correction could sit behind
|
|
@@ -2660,11 +2716,14 @@ class CliHostRuntime {
|
|
|
2660
2716
|
// batch uses the LAST entry's projectPath/handlers/launchContext unless a
|
|
2661
2717
|
// 'stop' entry is present, in which case that correction's context wins —
|
|
2662
2718
|
// it is the manager's most recent, most urgent instruction.
|
|
2663
|
-
dequeueNext(
|
|
2664
|
-
|
|
2719
|
+
dequeueNext(hostId, runEntry) {
|
|
2720
|
+
const sessionId = runEntry.sessionId;
|
|
2721
|
+
this.clearActiveRun(runEntry);
|
|
2665
2722
|
const batch = runEntry.pending.splice(0);
|
|
2666
2723
|
if (!batch.length)
|
|
2667
2724
|
return;
|
|
2725
|
+
if (!sessionId)
|
|
2726
|
+
return;
|
|
2668
2727
|
const combinedMessage = batch.map((e) => e.message).join('\n\n');
|
|
2669
2728
|
const primary = batch.find((e) => e.deliveryIntent === 'stop') ?? batch[batch.length - 1];
|
|
2670
2729
|
try {
|
|
@@ -2674,7 +2733,7 @@ class CliHostRuntime {
|
|
|
2674
2733
|
// R19: a synchronous spawn failure must not leave the queue permanently
|
|
2675
2734
|
// stuck behind this key — clear it so the next continueRun call spawns
|
|
2676
2735
|
// fresh instead of queueing behind a dead entry.
|
|
2677
|
-
this.
|
|
2736
|
+
this.clearActiveRun(runEntry);
|
|
2678
2737
|
}
|
|
2679
2738
|
}
|
|
2680
2739
|
}
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.HUB_APP_ENTRY_RELATIVE = exports.HUB_APP_PACKAGE_NAME = void 0;
|
|
7
|
+
exports.redactCredentialLikeSubstrings = redactCredentialLikeSubstrings;
|
|
7
8
|
exports.resolvePackagedCliScriptPath = resolvePackagedCliScriptPath;
|
|
8
9
|
exports.planHubAppMaterialization = planHubAppMaterialization;
|
|
9
10
|
exports.resolveHubAppCacheDir = resolveHubAppCacheDir;
|
|
@@ -33,6 +34,23 @@ const managed_node_runtime_1 = require("../cli/utils/managed-node-runtime");
|
|
|
33
34
|
// offline matrix is unit-testable without touching npm, disk, or Electron — the same separation
|
|
34
35
|
// `hub-launch-decision.ts` (#755) and `desktop-auto-updater.ts` (#1387) already established for
|
|
35
36
|
// this exact class of problem.
|
|
37
|
+
// Issue #1627 (security review): the raw materialization failure text below is now surfaced to a
|
|
38
|
+
// new, wider audience than before this issue — a persisted log file and the manual restart-to-
|
|
39
|
+
// latest HTTP response/UI — where previously it was silently discarded. `npm install` output does
|
|
40
|
+
// not normally echo credentials, but a misconfigured corporate registry (a literal token in a
|
|
41
|
+
// `.npmrc` URL, verbose proxy auth errors) is a real enough edge case that this new, wider surface
|
|
42
|
+
// should not forward it as-is. Defense-in-depth only: redact credential-shaped substrings before
|
|
43
|
+
// this text reaches either destination.
|
|
44
|
+
const CREDENTIAL_LIKE_PATTERNS = [
|
|
45
|
+
/:\/\/[^\s/@]+:[^\s/@]+@/g, // scheme://user:pass@ basic-auth-in-URL
|
|
46
|
+
/\b(?:_authToken|authToken|Authorization|api[_-]?key|password)\s*[:=]\s*\S+/gi,
|
|
47
|
+
/\bnpm_[A-Za-z0-9]{20,}\b/g, // npm access-token shape
|
|
48
|
+
/\bBearer\s+[A-Za-z0-9._-]{10,}\b/gi,
|
|
49
|
+
];
|
|
50
|
+
/** Strip anything that looks like a credential from text before it is logged or surfaced. */
|
|
51
|
+
function redactCredentialLikeSubstrings(text) {
|
|
52
|
+
return CREDENTIAL_LIKE_PATTERNS.reduce((acc, pattern) => acc.replace(pattern, '[redacted]'), text);
|
|
53
|
+
}
|
|
36
54
|
exports.HUB_APP_PACKAGE_NAME = 'fraim-hub';
|
|
37
55
|
/** Relative path, from a materialized (or bundled) package root, to the Electron main entry. */
|
|
38
56
|
exports.HUB_APP_ENTRY_RELATIVE = path_1.default.join('dist', 'src', 'ai-hub', 'desktop-main.js');
|
|
@@ -68,12 +86,14 @@ function planHubAppMaterialization(input) {
|
|
|
68
86
|
action: 'use-cached',
|
|
69
87
|
version: newestCached,
|
|
70
88
|
reason: 'npm registry unreachable; reusing the newest complete cached version',
|
|
89
|
+
degraded: false,
|
|
71
90
|
};
|
|
72
91
|
}
|
|
73
92
|
return {
|
|
74
93
|
action: 'use-bundled',
|
|
75
94
|
version: bundledVersion,
|
|
76
95
|
reason: 'npm registry unreachable and no cached version available; falling back to the version bundled with this installer',
|
|
96
|
+
degraded: true,
|
|
77
97
|
};
|
|
78
98
|
}
|
|
79
99
|
if (latestPublishedVersion === bundledVersion) {
|
|
@@ -81,6 +101,7 @@ function planHubAppMaterialization(input) {
|
|
|
81
101
|
action: 'use-bundled',
|
|
82
102
|
version: bundledVersion,
|
|
83
103
|
reason: 'the installed shell already matches the latest published version',
|
|
104
|
+
degraded: false,
|
|
84
105
|
};
|
|
85
106
|
}
|
|
86
107
|
const alreadyMaterialized = cachedVersions.some((entry) => entry.complete && entry.version === latestPublishedVersion);
|
|
@@ -89,12 +110,14 @@ function planHubAppMaterialization(input) {
|
|
|
89
110
|
action: 'use-cached',
|
|
90
111
|
version: latestPublishedVersion,
|
|
91
112
|
reason: 'latest published version is already materialized on disk',
|
|
113
|
+
degraded: false,
|
|
92
114
|
};
|
|
93
115
|
}
|
|
94
116
|
return {
|
|
95
117
|
action: 'materialize',
|
|
96
118
|
version: latestPublishedVersion,
|
|
97
119
|
reason: `latest published version ${latestPublishedVersion} is not yet materialized`,
|
|
120
|
+
degraded: false,
|
|
98
121
|
};
|
|
99
122
|
}
|
|
100
123
|
// ---------------------------------------------------------------------------
|
|
@@ -265,7 +288,11 @@ async function resolveHubAppEntry(options) {
|
|
|
265
288
|
cachedVersions,
|
|
266
289
|
});
|
|
267
290
|
if (plan.action === 'use-bundled') {
|
|
268
|
-
|
|
291
|
+
const materializationError = plan.degraded ? plan.reason : undefined;
|
|
292
|
+
if (materializationError) {
|
|
293
|
+
console.error(`[fraim] FRAIM Hub update check could not complete: ${materializationError}`);
|
|
294
|
+
}
|
|
295
|
+
return { entryPath: options.bundledEntryPath, source: 'bundled', version: plan.version, materializationError };
|
|
269
296
|
}
|
|
270
297
|
if (plan.action === 'use-cached') {
|
|
271
298
|
return {
|
|
@@ -279,7 +306,10 @@ async function resolveHubAppEntry(options) {
|
|
|
279
306
|
const entryPath = await ensureMaterializedHubApp(plan.version, { fraimDir, installPackage: options.installPackage });
|
|
280
307
|
return { entryPath, source: 'cached', version: plan.version };
|
|
281
308
|
}
|
|
282
|
-
catch {
|
|
283
|
-
|
|
309
|
+
catch (error) {
|
|
310
|
+
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
311
|
+
const materializationError = redactCredentialLikeSubstrings(rawMessage);
|
|
312
|
+
console.error(`[fraim] failed to materialize FRAIM Hub ${plan.version}; falling back to bundled ${options.bundledVersion}: ${materializationError}`);
|
|
313
|
+
return { entryPath: options.bundledEntryPath, source: 'bundled', version: options.bundledVersion, materializationError };
|
|
284
314
|
}
|
|
285
315
|
}
|