bosun 0.41.8 → 0.41.10
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/.env.example +1 -1
- package/README.md +23 -1
- package/agent/agent-event-bus.mjs +31 -2
- package/agent/agent-pool.mjs +275 -40
- package/agent/agent-prompts.mjs +9 -1
- package/agent/agent-supervisor.mjs +22 -0
- package/agent/autofix.mjs +1 -1
- package/agent/primary-agent.mjs +115 -5
- package/cli.mjs +3 -2
- package/config/config.mjs +47 -33
- package/config/context-shredding-config.mjs +1 -1
- package/config/repo-root.mjs +41 -33
- package/desktop/main.mjs +350 -25
- package/desktop/preload.cjs +8 -0
- package/desktop/preload.mjs +19 -0
- package/entrypoint.mjs +332 -0
- package/git/sdk-conflict-resolver.mjs +1 -1
- package/infra/health-status.mjs +72 -0
- package/infra/library-manager.mjs +58 -1
- package/infra/maintenance.mjs +1 -2
- package/infra/monitor.mjs +26 -8
- package/infra/session-tracker.mjs +30 -3
- package/package.json +12 -4
- package/server/bosun-mcp-server.mjs +1004 -0
- package/server/setup-web-server.mjs +288 -259
- package/server/ui-server.mjs +1323 -26
- package/shell/claude-shell.mjs +14 -1
- package/shell/codex-config.mjs +1 -1
- package/shell/codex-model-profiles.mjs +170 -30
- package/shell/codex-shell.mjs +63 -18
- package/shell/opencode-providers.mjs +20 -8
- package/task/task-executor.mjs +28 -0
- package/task/task-store.mjs +13 -4
- package/telegram/telegram-sentinel.mjs +54 -3
- package/tools/list-todos.mjs +7 -1
- package/ui/app.js +3 -2
- package/ui/components/agent-selector.js +127 -0
- package/ui/components/session-list.js +15 -10
- package/ui/demo-defaults.js +334 -336
- package/ui/modules/router.js +2 -0
- package/ui/modules/state.js +13 -5
- package/ui/tabs/chat.js +3 -0
- package/ui/tabs/library.js +284 -52
- package/ui/tabs/tasks.js +5 -13
- package/ui/tabs/workflows.js +766 -3
- package/workflow/workflow-engine.mjs +246 -5
- package/workflow/workflow-nodes/definitions.mjs +37 -0
- package/workflow/workflow-nodes.mjs +1014 -184
- package/workflow/workflow-templates.mjs +0 -5
- package/workflow-templates/_helpers.mjs +253 -0
- package/workflow-templates/agents.mjs +199 -226
- package/workflow-templates/github.mjs +106 -16
- package/workflow-templates/sub-workflows.mjs +233 -0
- package/workflow-templates/task-execution.mjs +125 -471
- package/workflow-templates/task-lifecycle.mjs +11 -48
- package/workspace/command-diagnostics.mjs +460 -0
- package/workspace/context-cache.mjs +396 -28
- package/workspace/worktree-manager.mjs +1 -1
package/desktop/main.mjs
CHANGED
|
@@ -95,9 +95,12 @@ let runtimeConfigLoaded = false;
|
|
|
95
95
|
let trayMode = false;
|
|
96
96
|
/** True when the main window should start hidden (background mode). */
|
|
97
97
|
let startHidden = false;
|
|
98
|
+
/** True when connected to a user-configured remote Bosun instance. */
|
|
99
|
+
let remoteConnectionActive = false;
|
|
98
100
|
const DEFAULT_TELEGRAM_UI_PORT = 3080;
|
|
99
101
|
const DESKTOP_RELEASES_URL = "https://github.com/virtengine/bosun/releases";
|
|
100
102
|
const STARTUP_UPDATE_REMIND_LATER_MS = 12 * 60 * 60 * 1000;
|
|
103
|
+
const REMOTE_CONNECTION_FILE = "remote-connection.json";
|
|
101
104
|
|
|
102
105
|
/** @type {{
|
|
103
106
|
* checking: boolean,
|
|
@@ -199,18 +202,30 @@ function installDesktopAuthHeaderBridge() {
|
|
|
199
202
|
if (!ses) return;
|
|
200
203
|
ses.webRequest.onBeforeSendHeaders((details, callback) => {
|
|
201
204
|
try {
|
|
202
|
-
const desktopKey = String(process.env.BOSUN_DESKTOP_API_KEY || "").trim();
|
|
203
|
-
if (!desktopKey) {
|
|
204
|
-
callback({ requestHeaders: details.requestHeaders });
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
205
|
if (!isTrustedDesktopRequestUrl(details?.url || "")) {
|
|
208
206
|
callback({ requestHeaders: details.requestHeaders });
|
|
209
207
|
return;
|
|
210
208
|
}
|
|
211
209
|
const headers = { ...(details.requestHeaders || {}) };
|
|
212
210
|
const existingAuth = String(headers.Authorization || headers.authorization || "").trim();
|
|
213
|
-
if (
|
|
211
|
+
if (existingAuth) {
|
|
212
|
+
callback({ requestHeaders: headers });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// When connected to a remote instance, use the configured API key
|
|
217
|
+
if (remoteConnectionActive) {
|
|
218
|
+
const remote = readRemoteConnectionConfig();
|
|
219
|
+
if (remote.apiKey) {
|
|
220
|
+
headers["X-API-Key"] = remote.apiKey;
|
|
221
|
+
callback({ requestHeaders: headers });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Otherwise use the local desktop API key
|
|
227
|
+
const desktopKey = String(process.env.BOSUN_DESKTOP_API_KEY || "").trim();
|
|
228
|
+
if (desktopKey) {
|
|
214
229
|
headers.Authorization = `Bearer ${desktopKey}`;
|
|
215
230
|
}
|
|
216
231
|
callback({ requestHeaders: headers });
|
|
@@ -373,6 +388,66 @@ function ensureDesktopApiKeyInEnv() {
|
|
|
373
388
|
return current;
|
|
374
389
|
}
|
|
375
390
|
|
|
391
|
+
/* ── Remote-connection config persistence ─────────────────────── */
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Read the persisted remote-connection config.
|
|
395
|
+
* Returns { enabled: boolean, endpoint: string, apiKey: string }.
|
|
396
|
+
*/
|
|
397
|
+
function readRemoteConnectionConfig() {
|
|
398
|
+
try {
|
|
399
|
+
const file = resolve(resolveDesktopConfigDir(), REMOTE_CONNECTION_FILE);
|
|
400
|
+
if (!existsSync(file)) return { enabled: false, endpoint: "", apiKey: "" };
|
|
401
|
+
const raw = JSON.parse(readFileSync(file, "utf8"));
|
|
402
|
+
return {
|
|
403
|
+
enabled: !!raw?.enabled,
|
|
404
|
+
endpoint: String(raw?.endpoint || "").trim(),
|
|
405
|
+
apiKey: String(raw?.apiKey || "").trim(),
|
|
406
|
+
};
|
|
407
|
+
} catch {
|
|
408
|
+
return { enabled: false, endpoint: "", apiKey: "" };
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Persist remote-connection config to disk.
|
|
414
|
+
* @param {{ enabled: boolean, endpoint: string, apiKey: string }} config
|
|
415
|
+
*/
|
|
416
|
+
function saveRemoteConnectionConfig(config) {
|
|
417
|
+
const dir = resolveDesktopConfigDir();
|
|
418
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
419
|
+
const file = resolve(dir, REMOTE_CONNECTION_FILE);
|
|
420
|
+
const payload = {
|
|
421
|
+
enabled: !!config?.enabled,
|
|
422
|
+
endpoint: String(config?.endpoint || "").trim(),
|
|
423
|
+
apiKey: String(config?.apiKey || "").trim(),
|
|
424
|
+
};
|
|
425
|
+
writeFileSync(file, JSON.stringify(payload, null, 2), "utf8");
|
|
426
|
+
return payload;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Probe a remote Bosun endpoint. Returns true if reachable.
|
|
431
|
+
*/
|
|
432
|
+
async function probeRemoteEndpoint(endpoint, apiKey) {
|
|
433
|
+
try {
|
|
434
|
+
const url = new URL("/api/status", endpoint);
|
|
435
|
+
const headers = {};
|
|
436
|
+
if (apiKey) headers["x-api-key"] = apiKey;
|
|
437
|
+
const mod = url.protocol === "https:" ? await import("node:https") : await import("node:http");
|
|
438
|
+
return new Promise((ok) => {
|
|
439
|
+
const req = mod.get(url, { headers, timeout: 3000, rejectUnauthorized: false }, (res) => {
|
|
440
|
+
ok(res.statusCode >= 200 && res.statusCode < 400);
|
|
441
|
+
res.resume();
|
|
442
|
+
});
|
|
443
|
+
req.on("error", () => ok(false));
|
|
444
|
+
req.on("timeout", () => { req.destroy(); ok(false); });
|
|
445
|
+
});
|
|
446
|
+
} catch {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
376
451
|
function isProcessAlive(pid) {
|
|
377
452
|
try {
|
|
378
453
|
process.kill(pid, 0);
|
|
@@ -1261,7 +1336,7 @@ async function loadRuntimeConfig() {
|
|
|
1261
1336
|
|
|
1262
1337
|
async function loadUiServerModule() {
|
|
1263
1338
|
if (uiApi) return uiApi;
|
|
1264
|
-
uiApi = await loadBosunModule("ui-server.mjs");
|
|
1339
|
+
uiApi = await loadBosunModule("server/ui-server.mjs");
|
|
1265
1340
|
return uiApi;
|
|
1266
1341
|
}
|
|
1267
1342
|
|
|
@@ -1412,18 +1487,37 @@ async function startUiServer() {
|
|
|
1412
1487
|
|
|
1413
1488
|
async function buildUiUrl() {
|
|
1414
1489
|
await loadRuntimeConfig();
|
|
1490
|
+
|
|
1491
|
+
// ── Check for active remote connection config ──
|
|
1492
|
+
const remote = readRemoteConnectionConfig();
|
|
1493
|
+
if (remote.enabled && remote.endpoint) {
|
|
1494
|
+
const reachable = await probeRemoteEndpoint(remote.endpoint, remote.apiKey);
|
|
1495
|
+
if (reachable) {
|
|
1496
|
+
console.log("[desktop] using remote Bosun endpoint:", remote.endpoint);
|
|
1497
|
+
remoteConnectionActive = true;
|
|
1498
|
+
const remoteTarget = new URL(remote.endpoint);
|
|
1499
|
+
uiOrigin = remoteTarget.origin;
|
|
1500
|
+
// Do NOT put the API key in the URL — it leaks into logs, window
|
|
1501
|
+
// history, crash reports, and Referer headers. The desktop auth
|
|
1502
|
+
// header bridge (installDesktopAuthHeaderBridge) injects X-API-Key
|
|
1503
|
+
// on every request to this origin automatically.
|
|
1504
|
+
return remoteTarget.toString();
|
|
1505
|
+
}
|
|
1506
|
+
console.warn("[desktop] remote endpoint unreachable, falling back to local");
|
|
1507
|
+
remoteConnectionActive = false;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1415
1510
|
const daemonUrl = await resolveDaemonUiUrl();
|
|
1416
1511
|
if (daemonUrl) {
|
|
1417
1512
|
uiOrigin = new URL(daemonUrl).origin;
|
|
1418
|
-
//
|
|
1419
|
-
//
|
|
1513
|
+
// The desktop auth header bridge injects Authorization: Bearer <key>
|
|
1514
|
+
// on every request, so the initial page load will carry the header.
|
|
1515
|
+
// The server validates the header and sets a ve_session cookie, making
|
|
1516
|
+
// subsequent in-page fetches authenticated without URL secrets.
|
|
1420
1517
|
const desktopKey = ensureDesktopApiKeyInEnv();
|
|
1421
|
-
if (desktopKey) {
|
|
1422
|
-
|
|
1423
|
-
daemonTarget.searchParams.set("desktopKey", desktopKey);
|
|
1424
|
-
return daemonTarget.toString();
|
|
1518
|
+
if (!desktopKey) {
|
|
1519
|
+
console.warn("[desktop] BOSUN_DESKTOP_API_KEY unavailable; daemon UI may return 401 until auth bootstrap succeeds");
|
|
1425
1520
|
}
|
|
1426
|
-
console.warn("[desktop] BOSUN_DESKTOP_API_KEY unavailable; daemon UI may return 401 until auth bootstrap succeeds");
|
|
1427
1521
|
return daemonUrl;
|
|
1428
1522
|
}
|
|
1429
1523
|
// Daemon is not reachable — flag it so the window can show an offline banner.
|
|
@@ -1436,20 +1530,176 @@ async function buildUiUrl() {
|
|
|
1436
1530
|
}
|
|
1437
1531
|
const targetUrl = new URL(uiServerUrl);
|
|
1438
1532
|
uiOrigin = targetUrl.origin;
|
|
1439
|
-
//
|
|
1440
|
-
//
|
|
1441
|
-
|
|
1442
|
-
if (desktopKey) {
|
|
1443
|
-
targetUrl.searchParams.set("desktopKey", desktopKey);
|
|
1444
|
-
} else {
|
|
1445
|
-
const sessionToken = api.getSessionToken();
|
|
1446
|
-
if (sessionToken) {
|
|
1447
|
-
targetUrl.searchParams.set("token", sessionToken);
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1533
|
+
// Auth is handled by the header bridge. For the local embedded server,
|
|
1534
|
+
// the request arrives with Authorization: Bearer <desktopKey> and the
|
|
1535
|
+
// server validates + sets a ve_session cookie. No URL secrets needed.
|
|
1450
1536
|
return targetUrl.toString();
|
|
1451
1537
|
}
|
|
1452
1538
|
|
|
1539
|
+
/* ── Launch-time connection dialogs ──────────────────────────── */
|
|
1540
|
+
|
|
1541
|
+
/**
|
|
1542
|
+
* Show a dialog asking the user how to proceed when no local daemon is found.
|
|
1543
|
+
* Returns "local" | "remote" | "offline".
|
|
1544
|
+
*/
|
|
1545
|
+
async function showConnectionChoiceDialog() {
|
|
1546
|
+
const { response } = await dialog.showMessageBox(mainWindow, {
|
|
1547
|
+
type: "question",
|
|
1548
|
+
title: "Bosun — No Running Instance Detected",
|
|
1549
|
+
message: "Bosun couldn't find a running daemon.\nHow would you like to proceed?",
|
|
1550
|
+
detail: [
|
|
1551
|
+
"• Start Local — launch Bosun in this process (default)",
|
|
1552
|
+
"• Connect to Remote — connect to an external Bosun instance (Docker, VM, cloud)",
|
|
1553
|
+
"• Continue Offline — open the portal in offline mode",
|
|
1554
|
+
].join("\n"),
|
|
1555
|
+
buttons: ["Start Local", "Connect to Remote", "Continue Offline"],
|
|
1556
|
+
defaultId: 0,
|
|
1557
|
+
cancelId: 2,
|
|
1558
|
+
noLink: true,
|
|
1559
|
+
});
|
|
1560
|
+
if (response === 1) return "remote";
|
|
1561
|
+
if (response === 2) return "offline";
|
|
1562
|
+
return "local";
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
/**
|
|
1566
|
+
* Prompt the user for a remote Bosun endpoint URL and API key.
|
|
1567
|
+
* Returns { endpoint, apiKey } or null if cancelled.
|
|
1568
|
+
*/
|
|
1569
|
+
async function showRemoteConnectionInputDialog() {
|
|
1570
|
+
/* ── Step 1: Endpoint URL ── */
|
|
1571
|
+
const existing = readRemoteConnectionConfig();
|
|
1572
|
+
const endpointPrompt = await dialog.showMessageBox(mainWindow, {
|
|
1573
|
+
type: "question",
|
|
1574
|
+
title: "Remote Bosun — Enter Endpoint",
|
|
1575
|
+
message: "Enter the URL of the remote Bosun instance:",
|
|
1576
|
+
detail: `Example: https://bosun.example.com:3080\n\nCurrent: ${existing.endpoint || "(none)"}`,
|
|
1577
|
+
buttons: ["OK", "Cancel"],
|
|
1578
|
+
defaultId: 0,
|
|
1579
|
+
cancelId: 1,
|
|
1580
|
+
noLink: true,
|
|
1581
|
+
// Electron doesn't have an input dialog builtin, so we use a two-step
|
|
1582
|
+
// approach with a prompt window for the actual text input.
|
|
1583
|
+
});
|
|
1584
|
+
if (endpointPrompt.response === 1) return null;
|
|
1585
|
+
|
|
1586
|
+
// Use a small BrowserWindow as a text-input dialog
|
|
1587
|
+
const endpoint = await showTextInputWindow(
|
|
1588
|
+
"Remote Bosun — Endpoint URL",
|
|
1589
|
+
"Enter the Bosun endpoint URL:",
|
|
1590
|
+
existing.endpoint || "https://",
|
|
1591
|
+
"e.g. https://bosun.example.com:3080",
|
|
1592
|
+
);
|
|
1593
|
+
if (!endpoint) return null;
|
|
1594
|
+
|
|
1595
|
+
/* ── Step 2: API Key ── */
|
|
1596
|
+
const apiKey = await showTextInputWindow(
|
|
1597
|
+
"Remote Bosun — API Key",
|
|
1598
|
+
"Enter the API key (BOSUN_API_KEY) for authentication:",
|
|
1599
|
+
existing.apiKey || "",
|
|
1600
|
+
"Leave empty if none is required",
|
|
1601
|
+
);
|
|
1602
|
+
// apiKey can be empty — that's valid (server may have auth disabled)
|
|
1603
|
+
|
|
1604
|
+
// Validate connectivity before saving
|
|
1605
|
+
const reachable = await probeRemoteEndpoint(endpoint, apiKey || "");
|
|
1606
|
+
if (!reachable) {
|
|
1607
|
+
await dialog.showMessageBox(mainWindow, {
|
|
1608
|
+
type: "warning",
|
|
1609
|
+
title: "Connection Failed",
|
|
1610
|
+
message: `Could not reach ${endpoint}`,
|
|
1611
|
+
detail: "The remote instance may be offline or the URL/API key may be incorrect.\nThe config has been saved — you can update it in Settings.",
|
|
1612
|
+
buttons: ["OK"],
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
return { endpoint, apiKey: apiKey || "" };
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
/**
|
|
1619
|
+
* Show a small modal window with a text input field.
|
|
1620
|
+
* Returns the entered string or null if cancelled.
|
|
1621
|
+
*/
|
|
1622
|
+
function showTextInputWindow(title, label, defaultValue, placeholder) {
|
|
1623
|
+
return new Promise((resolvePromise) => {
|
|
1624
|
+
const parent = mainWindow || undefined;
|
|
1625
|
+
const inputWin = new BrowserWindow({
|
|
1626
|
+
width: 520,
|
|
1627
|
+
height: 210,
|
|
1628
|
+
resizable: false,
|
|
1629
|
+
minimizable: false,
|
|
1630
|
+
maximizable: false,
|
|
1631
|
+
modal: !!parent,
|
|
1632
|
+
parent,
|
|
1633
|
+
show: false,
|
|
1634
|
+
backgroundColor: "#1a1a2e",
|
|
1635
|
+
autoHideMenuBar: true,
|
|
1636
|
+
webPreferences: {
|
|
1637
|
+
contextIsolation: true,
|
|
1638
|
+
nodeIntegration: false,
|
|
1639
|
+
sandbox: true,
|
|
1640
|
+
},
|
|
1641
|
+
});
|
|
1642
|
+
const escapeHtml = (value) =>
|
|
1643
|
+
String(value ?? "")
|
|
1644
|
+
.replace(/&/g, "&")
|
|
1645
|
+
.replace(/</g, "<")
|
|
1646
|
+
.replace(/>/g, ">")
|
|
1647
|
+
.replace(/"/g, """)
|
|
1648
|
+
.replace(/'/g, "'");
|
|
1649
|
+
const escapedLabel = escapeHtml(label);
|
|
1650
|
+
const escapedDefault = escapeHtml(defaultValue);
|
|
1651
|
+
const escapedPlaceholder = escapeHtml(placeholder);
|
|
1652
|
+
const html = `data:text/html;charset=utf-8,${encodeURIComponent(`<!DOCTYPE html>
|
|
1653
|
+
<html><head><style>
|
|
1654
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
1655
|
+
body { font: 14px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
1656
|
+
background: #1a1a2e; color: #e0e0e0; padding: 20px; display: flex;
|
|
1657
|
+
flex-direction: column; height: 100vh; }
|
|
1658
|
+
label { display: block; margin-bottom: 8px; font-weight: 600; }
|
|
1659
|
+
input { width: 100%; padding: 8px 12px; font-size: 14px; border: 1px solid #444;
|
|
1660
|
+
border-radius: 6px; background: #0f0f23; color: #e0e0e0; outline: none; }
|
|
1661
|
+
input:focus { border-color: #6366f1; }
|
|
1662
|
+
.btns { display: flex; gap: 10px; justify-content: flex-end; margin-top: auto; }
|
|
1663
|
+
button { padding: 8px 20px; border: none; border-radius: 6px; font-size: 14px;
|
|
1664
|
+
cursor: pointer; font-weight: 500; }
|
|
1665
|
+
.ok { background: #4f46e5; color: #fff; }
|
|
1666
|
+
.ok:hover { background: #6366f1; }
|
|
1667
|
+
.cancel { background: #333; color: #ccc; }
|
|
1668
|
+
.cancel:hover { background: #444; }
|
|
1669
|
+
</style></head><body>
|
|
1670
|
+
<label>${escapedLabel}</label>
|
|
1671
|
+
<input id="val" type="text" value="${escapedDefault}" placeholder="${escapedPlaceholder}" autofocus />
|
|
1672
|
+
<div class="btns">
|
|
1673
|
+
<button class="cancel" onclick="close()">Cancel</button>
|
|
1674
|
+
<button class="ok" onclick="submit()">OK</button>
|
|
1675
|
+
</div>
|
|
1676
|
+
<script>
|
|
1677
|
+
const inp = document.getElementById('val');
|
|
1678
|
+
inp.select();
|
|
1679
|
+
function submit() { document.title = 'RESULT:' + inp.value; }
|
|
1680
|
+
function close() { document.title = 'CANCEL'; }
|
|
1681
|
+
inp.addEventListener('keydown', e => { if (e.key === 'Enter') submit(); if (e.key === 'Escape') close(); });
|
|
1682
|
+
</script>
|
|
1683
|
+
</body></html>`)}`;
|
|
1684
|
+
|
|
1685
|
+
inputWin.loadURL(html);
|
|
1686
|
+
inputWin.once("ready-to-show", () => inputWin.show());
|
|
1687
|
+
|
|
1688
|
+
inputWin.webContents.on("page-title-updated", (_ev, newTitle) => {
|
|
1689
|
+
if (newTitle.startsWith("RESULT:")) {
|
|
1690
|
+
const val = newTitle.slice(7).trim();
|
|
1691
|
+
inputWin.destroy();
|
|
1692
|
+
resolvePromise(val || null);
|
|
1693
|
+
} else if (newTitle === "CANCEL") {
|
|
1694
|
+
inputWin.destroy();
|
|
1695
|
+
resolvePromise(null);
|
|
1696
|
+
}
|
|
1697
|
+
});
|
|
1698
|
+
|
|
1699
|
+
inputWin.on("closed", () => resolvePromise(null));
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1453
1703
|
async function createMainWindow() {
|
|
1454
1704
|
if (mainWindow) return;
|
|
1455
1705
|
const iconPath = resolveDesktopIconPath();
|
|
@@ -1530,6 +1780,28 @@ async function createMainWindow() {
|
|
|
1530
1780
|
await mainWindow.loadURL(buildLoadingPageUrl("Starting Bosun services..."));
|
|
1531
1781
|
await setLoadingMessage("Preparing background daemon...");
|
|
1532
1782
|
await ensureDaemonRunning();
|
|
1783
|
+
|
|
1784
|
+
// ── If daemon is offline, check remote config or prompt user ──
|
|
1785
|
+
if (bosunDaemonWasOffline) {
|
|
1786
|
+
const remote = readRemoteConnectionConfig();
|
|
1787
|
+
const remoteReachable = remote.enabled && remote.endpoint
|
|
1788
|
+
? await probeRemoteEndpoint(remote.endpoint, remote.apiKey)
|
|
1789
|
+
: false;
|
|
1790
|
+
|
|
1791
|
+
if (!remoteReachable) {
|
|
1792
|
+
await setLoadingMessage("No running Bosun instance detected...");
|
|
1793
|
+
const choice = await showConnectionChoiceDialog();
|
|
1794
|
+
if (choice === "remote") {
|
|
1795
|
+
const config = await showRemoteConnectionInputDialog();
|
|
1796
|
+
if (config) {
|
|
1797
|
+
saveRemoteConnectionConfig({ ...config, enabled: true });
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
// "local" choice: we already tried; buildUiUrl will fall back to in-process server
|
|
1801
|
+
// "offline" choice: continue with in-process server
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1533
1805
|
await setLoadingMessage("Connecting to Bosun portal...");
|
|
1534
1806
|
const uiUrl = await buildUiUrl();
|
|
1535
1807
|
await mainWindow.loadURL(uiUrl);
|
|
@@ -2100,6 +2372,56 @@ function registerDesktopIpc() {
|
|
|
2100
2372
|
if (!workspaceId) return { ok: false, error: "workspaceId required" };
|
|
2101
2373
|
return switchWorkspace(workspaceId);
|
|
2102
2374
|
});
|
|
2375
|
+
|
|
2376
|
+
// ── Connection Settings IPC ──────────────────────────────────────────
|
|
2377
|
+
/** Get the current remote connection config. */
|
|
2378
|
+
ipcMain.handle("bosun:connection:get", () => {
|
|
2379
|
+
const config = readRemoteConnectionConfig();
|
|
2380
|
+
return { ok: true, ...config, active: remoteConnectionActive };
|
|
2381
|
+
});
|
|
2382
|
+
|
|
2383
|
+
/**
|
|
2384
|
+
* Update the remote connection config.
|
|
2385
|
+
* Payload: { enabled: boolean, endpoint: string, apiKey: string }
|
|
2386
|
+
*/
|
|
2387
|
+
ipcMain.handle("bosun:connection:set", async (_event, config) => {
|
|
2388
|
+
try {
|
|
2389
|
+
const saved = saveRemoteConnectionConfig(config);
|
|
2390
|
+
// If switching to remote, update origin tracking flag
|
|
2391
|
+
if (saved.enabled && saved.endpoint) {
|
|
2392
|
+
remoteConnectionActive = true;
|
|
2393
|
+
} else {
|
|
2394
|
+
remoteConnectionActive = false;
|
|
2395
|
+
}
|
|
2396
|
+
return { ok: true, ...saved };
|
|
2397
|
+
} catch (err) {
|
|
2398
|
+
return { ok: false, error: err?.message || "Failed to save connection config" };
|
|
2399
|
+
}
|
|
2400
|
+
});
|
|
2401
|
+
|
|
2402
|
+
/**
|
|
2403
|
+
* Test connectivity to a remote Bosun endpoint.
|
|
2404
|
+
* Payload: { endpoint: string, apiKey: string }
|
|
2405
|
+
*/
|
|
2406
|
+
ipcMain.handle("bosun:connection:test", async (_event, { endpoint, apiKey } = {}) => {
|
|
2407
|
+
if (!endpoint) return { ok: false, error: "endpoint required" };
|
|
2408
|
+
const reachable = await probeRemoteEndpoint(endpoint, apiKey || "");
|
|
2409
|
+
return { ok: true, reachable };
|
|
2410
|
+
});
|
|
2411
|
+
|
|
2412
|
+
/**
|
|
2413
|
+
* Open the remote connection setup dialog.
|
|
2414
|
+
* Returns the config if set, or null if cancelled.
|
|
2415
|
+
*/
|
|
2416
|
+
ipcMain.handle("bosun:connection:setup", async () => {
|
|
2417
|
+
const config = await showRemoteConnectionInputDialog();
|
|
2418
|
+
if (config) {
|
|
2419
|
+
saveRemoteConnectionConfig({ ...config, enabled: true });
|
|
2420
|
+
remoteConnectionActive = true;
|
|
2421
|
+
return { ok: true, ...config, enabled: true };
|
|
2422
|
+
}
|
|
2423
|
+
return { ok: false, cancelled: true };
|
|
2424
|
+
});
|
|
2103
2425
|
}
|
|
2104
2426
|
|
|
2105
2427
|
async function bootstrap() {
|
|
@@ -2108,6 +2430,9 @@ async function bootstrap() {
|
|
|
2108
2430
|
// — before any network request is made (config loading, API key probe, etc.).
|
|
2109
2431
|
// allow-insecure-localhost (set pre-ready above) handles 127.0.0.1; this
|
|
2110
2432
|
// setCertificateVerifyProc covers LAN IPs (192.168.x.x / 10.x etc.).
|
|
2433
|
+
//
|
|
2434
|
+
// SECURITY: Only bypass TLS verification for localhost and private-network
|
|
2435
|
+
// addresses. Remote/public endpoints must pass standard chain verification.
|
|
2111
2436
|
session.defaultSession.setCertificateVerifyProc((request, callback) => {
|
|
2112
2437
|
if (isLocalHost(request.hostname)) {
|
|
2113
2438
|
callback(0); // 0 = verified OK
|
package/desktop/preload.cjs
CHANGED
|
@@ -23,4 +23,12 @@ contextBridge.exposeInMainWorld("veDesktop", {
|
|
|
23
23
|
resetAll: () => ipcRenderer.invoke("bosun:shortcuts:resetAll"),
|
|
24
24
|
showDialog: () => ipcRenderer.invoke("bosun:shortcuts:showDialog"),
|
|
25
25
|
},
|
|
26
|
+
|
|
27
|
+
connection: {
|
|
28
|
+
get: () => ipcRenderer.invoke("bosun:connection:get"),
|
|
29
|
+
set: (config) => ipcRenderer.invoke("bosun:connection:set", config),
|
|
30
|
+
test: (endpoint, apiKey) =>
|
|
31
|
+
ipcRenderer.invoke("bosun:connection:test", { endpoint, apiKey }),
|
|
32
|
+
setup: () => ipcRenderer.invoke("bosun:connection:setup"),
|
|
33
|
+
},
|
|
26
34
|
});
|
package/desktop/preload.mjs
CHANGED
|
@@ -94,4 +94,23 @@ contextBridge.exposeInMainWorld("veDesktop", {
|
|
|
94
94
|
setScope: (id, isGlobal) =>
|
|
95
95
|
ipcRenderer.invoke("bosun:shortcuts:setScope", { id, isGlobal }),
|
|
96
96
|
},
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Remote connection settings API.
|
|
100
|
+
* Available in the renderer via `window.veDesktop.connection.*`
|
|
101
|
+
*/
|
|
102
|
+
connection: {
|
|
103
|
+
/** Get the current remote connection config + active status. */
|
|
104
|
+
get: () => ipcRenderer.invoke("bosun:connection:get"),
|
|
105
|
+
|
|
106
|
+
/** Save remote connection config. { enabled, endpoint, apiKey } */
|
|
107
|
+
set: (config) => ipcRenderer.invoke("bosun:connection:set", config),
|
|
108
|
+
|
|
109
|
+
/** Test connectivity to a remote endpoint. { endpoint, apiKey } */
|
|
110
|
+
test: (endpoint, apiKey) =>
|
|
111
|
+
ipcRenderer.invoke("bosun:connection:test", { endpoint, apiKey }),
|
|
112
|
+
|
|
113
|
+
/** Open the interactive remote connection setup dialog. */
|
|
114
|
+
setup: () => ipcRenderer.invoke("bosun:connection:setup"),
|
|
115
|
+
},
|
|
97
116
|
});
|