Skip to content

Commit ba2c2a2

Browse files
committed
release: fix desktop local auth startup
1 parent a7dcb88 commit ba2c2a2

File tree

9 files changed

+177
-19
lines changed

9 files changed

+177
-19
lines changed

apps/app/electrobun/scripts/smoke-test-windows.ps1

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,11 @@ function Dump-PortDiagnostics([int]$Port) {
258258
Write-Host "--- end netstat ---"
259259
}
260260

261+
function Test-BackendProbeStatus([int]$StatusCode) {
262+
# A 401 still proves the packaged backend is running and enforcing auth.
263+
return $StatusCode -eq 200 -or $StatusCode -eq 401
264+
}
265+
261266
function Dump-ProcessDiagnostics() {
262267
Write-Host "--- Bun/launcher processes ---"
263268
try {
@@ -398,10 +403,11 @@ try {
398403
$client.Timeout = [TimeSpan]::FromSeconds(3)
399404
$task = $client.GetAsync($uri)
400405
$task.Wait()
401-
if ($task.Result.IsSuccessStatusCode) {
406+
$statusCode = [int]$task.Result.StatusCode
407+
if (Test-BackendProbeStatus $statusCode) {
402408
$healthy = $true
403409
$healthCheckMethod = "HttpClient(no-proxy)"
404-
Write-Host "Backend health check passed on port $port (via HttpClient, proxy bypassed)."
410+
Write-Host "Backend health check passed on port $port (via HttpClient, proxy bypassed, HTTP $statusCode)."
405411
break
406412
}
407413
} catch {
@@ -418,10 +424,10 @@ try {
418424
if (-not $healthy) {
419425
try {
420426
$curlResult = & "$env:SystemRoot\System32\curl.exe" -s -o NUL -w "%{http_code}" $uri --connect-timeout 3 --noproxy "127.0.0.1" 2>$null
421-
if ($curlResult -eq "200") {
427+
if ($curlResult -eq "200" -or $curlResult -eq "401") {
422428
$healthy = $true
423429
$healthCheckMethod = "curl.exe"
424-
Write-Host "Backend health check passed on port $port (via curl.exe)."
430+
Write-Host "Backend health check passed on port $port (via curl.exe, HTTP $curlResult)."
425431
break
426432
}
427433
} catch {}
@@ -430,11 +436,11 @@ try {
430436
# Method 3: Invoke-WebRequest with -NoProxy (PowerShell 7+).
431437
if (-not $healthy) {
432438
try {
433-
$response = Invoke-WebRequest -Uri $uri -UseBasicParsing -TimeoutSec 3 -NoProxy
434-
if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 300) {
439+
$response = Invoke-WebRequest -Uri $uri -UseBasicParsing -TimeoutSec 3 -NoProxy -SkipHttpErrorCheck
440+
if (Test-BackendProbeStatus ([int]$response.StatusCode)) {
435441
$healthy = $true
436442
$healthCheckMethod = "Invoke-WebRequest(-NoProxy)"
437-
Write-Host "Backend health check passed on port $port (via Invoke-WebRequest -NoProxy)."
443+
Write-Host "Backend health check passed on port $port (via Invoke-WebRequest -NoProxy, HTTP $($response.StatusCode))."
438444
break
439445
}
440446
} catch {}

apps/app/electrobun/scripts/smoke-test.sh

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,19 @@ attach_dmg_with_retry() {
9494
return 1
9595
}
9696

97+
backend_health_probe_status() {
98+
local url="$1"
99+
curl -sS -o /dev/null -w "%{http_code}" --connect-timeout 3 "$url" 2>/dev/null || printf "000"
100+
}
101+
102+
backend_health_probe_satisfied() {
103+
local url="$1"
104+
local status
105+
status="$(backend_health_probe_status "$url")"
106+
# A 401 still proves the packaged backend is running and enforcing auth.
107+
[[ "$status" == "200" || "$status" == "401" ]]
108+
}
109+
97110
ensure_diagnostics_dir() {
98111
if [[ -z "$SMOKE_DIAGNOSTICS_DIR" ]]; then
99112
SMOKE_DIAGNOSTICS_DIR="$(mktemp -d /tmp/milady-smoke-diagnostics.XXXXXX)"
@@ -672,7 +685,7 @@ while [[ $SECONDS -lt $DEADLINE ]]; do
672685
fi
673686
fi
674687
if [[ -n "$BACKEND_PORT" ]]; then
675-
if curl -fsS "http://127.0.0.1:${BACKEND_PORT}/api/health" >/dev/null; then
688+
if backend_health_probe_satisfied "http://127.0.0.1:${BACKEND_PORT}/api/health"; then
676689
echo "Backend health check PASSED on port $BACKEND_PORT."
677690
break
678691
fi
@@ -690,7 +703,7 @@ if [[ -z "$BACKEND_PORT" ]]; then
690703
exit 1
691704
fi
692705

693-
if ! curl -fsS "http://127.0.0.1:${BACKEND_PORT}/api/health" >/dev/null; then
706+
if ! backend_health_probe_satisfied "http://127.0.0.1:${BACKEND_PORT}/api/health"; then
694707
echo "ERROR: Backend did not answer /api/health on port $BACKEND_PORT"
695708
[[ -f "$STARTUP_LOG" ]] && tail -n 120 "$STARTUP_LOG"
696709
echo ""
@@ -732,7 +745,7 @@ echo "Waiting ${LIVENESS_TIMEOUT}s for liveness..."
732745
sleep "$LIVENESS_TIMEOUT"
733746
LIVE_PID="$(find_live_packaged_pid)"
734747
if [[ -n "$LIVE_PID" ]] && kill -0 "$LIVE_PID" 2>/dev/null; then
735-
if curl -fsS "http://127.0.0.1:${BACKEND_PORT}/api/health" >/dev/null; then
748+
if backend_health_probe_satisfied "http://127.0.0.1:${BACKEND_PORT}/api/health"; then
736749
echo "App process ($LIVE_PID) and backend still healthy after ${LIVENESS_TIMEOUT}s — liveness check PASSED."
737750
else
738751
echo "ERROR: App stayed open but backend health check failed after ${LIVENESS_TIMEOUT}s."
@@ -743,7 +756,7 @@ if [[ -n "$LIVE_PID" ]] && kill -0 "$LIVE_PID" 2>/dev/null; then
743756
dump_failure_diagnostics "backend liveness check failed after startup"
744757
exit 1
745758
fi
746-
elif curl -fsS "http://127.0.0.1:${BACKEND_PORT}/api/health" >/dev/null; then
759+
elif backend_health_probe_satisfied "http://127.0.0.1:${BACKEND_PORT}/api/health"; then
747760
echo "WARNING: No packaged app process was detected after ${LIVENESS_TIMEOUT}s, but the packaged backend remained healthy."
748761
echo " Treating backend liveness as the release gate for this launcher path."
749762
else

apps/app/electrobun/src/__tests__/agent.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,60 @@ describe("AgentManager", () => {
353353
expect(mockSpawn).toHaveBeenCalledTimes(1);
354354
});
355355

356+
it("authenticates local health and agent probes with the desktop API token", async () => {
357+
const originalMiladyToken = process.env.MILADY_API_TOKEN;
358+
const originalElizaToken = process.env.ELIZA_API_TOKEN;
359+
process.env.MILADY_API_TOKEN = "desktop-local-token";
360+
delete process.env.ELIZA_API_TOKEN;
361+
362+
try {
363+
const mockProc = createMockProcess();
364+
mockSpawn.mockReturnValue(mockProc);
365+
366+
mockFetch.mockResolvedValueOnce(makeHealthyResponse());
367+
mockFetch.mockResolvedValueOnce({
368+
ok: true,
369+
json: async () => ({ agents: [{ name: "Milady" }] }),
370+
});
371+
372+
await manager.start();
373+
374+
const expectedHeaders = {
375+
Authorization: "Bearer desktop-local-token",
376+
"X-Api-Key": "desktop-local-token",
377+
"X-Api-Token": "desktop-local-token",
378+
};
379+
380+
expect(mockFetch).toHaveBeenNthCalledWith(
381+
1,
382+
"http://127.0.0.1:2138/api/health",
383+
expect.objectContaining({
384+
headers: expectedHeaders,
385+
signal: expect.anything(),
386+
}),
387+
);
388+
expect(mockFetch).toHaveBeenNthCalledWith(
389+
2,
390+
"http://127.0.0.1:2138/api/agents",
391+
expect.objectContaining({
392+
headers: expectedHeaders,
393+
signal: expect.anything(),
394+
}),
395+
);
396+
} finally {
397+
if (originalMiladyToken === undefined) {
398+
delete process.env.MILADY_API_TOKEN;
399+
} else {
400+
process.env.MILADY_API_TOKEN = originalMiladyToken;
401+
}
402+
if (originalElizaToken === undefined) {
403+
delete process.env.ELIZA_API_TOKEN;
404+
} else {
405+
process.env.ELIZA_API_TOKEN = originalElizaToken;
406+
}
407+
}
408+
});
409+
356410
it("spawns bun process with the canonical runtime entry when present", async () => {
357411
const mockProc = createMockProcess();
358412
mockSpawn.mockReturnValue(mockProc);

apps/app/electrobun/src/__tests__/smoke-test-script.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,16 @@ describe("smoke-test.sh", () => {
6565
);
6666
});
6767

68+
it("treats auth-protected health probes as proof the packaged backend is alive", () => {
69+
const script = fs.readFileSync(SMOKE_TEST_PATH, "utf8");
70+
71+
expect(script).toContain("backend_health_probe_satisfied() {");
72+
expect(script).toContain(
73+
"# A 401 still proves the packaged backend is running and enforcing auth.",
74+
);
75+
expect(script).toContain('[[ "$status" == "200" || "$status" == "401" ]]');
76+
});
77+
6878
it("asserts the packaged bundle and executables keep the Milady identifier", () => {
6979
const script = fs.readFileSync(SMOKE_TEST_PATH, "utf8");
7080

apps/app/electrobun/src/index.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import {
3333
CloudAuthWindowManager,
3434
readNavigationEventUrl,
3535
} from "./cloud-auth-window";
36-
import { ensureDesktopApiToken, getAgentManager } from "./native/agent";
36+
import { configureDesktopLocalApiAuth, getAgentManager } from "./native/agent";
3737
import { getDesktopManager } from "./native/desktop";
3838
import { disposeNativeModules, initializeNativeModules } from "./native/index";
3939
import {
@@ -124,7 +124,8 @@ function buildApiRequestHeaders(contentType?: string): Record<string, string> {
124124
if (contentType) {
125125
headers["Content-Type"] = contentType;
126126
}
127-
const apiToken = process.env.MILADY_API_TOKEN?.trim();
127+
const apiToken =
128+
process.env.MILADY_API_TOKEN?.trim() ?? process.env.ELIZA_API_TOKEN?.trim();
128129
if (apiToken) {
129130
headers.Authorization = `Bearer ${apiToken}`;
130131
}
@@ -415,13 +416,20 @@ async function startRendererServer(): Promise<string> {
415416
const initialApiBase = resolveInitialApiBase(
416417
process.env as Record<string, string | undefined>,
417418
);
419+
const initialApiToken =
420+
resolveDesktopRuntimeMode(process.env as Record<string, string | undefined>)
421+
.mode === "local"
422+
? configureDesktopLocalApiAuth()
423+
: (process.env.MILADY_API_TOKEN?.trim() ??
424+
process.env.ELIZA_API_TOKEN?.trim() ??
425+
"");
418426

419427
// Inject the API base into index.html so it's available before React mounts.
420428
function injectApiBaseIntoHtml(html: string): string {
421429
if (!initialApiBase) {
422430
return html;
423431
}
424-
const script = `<script>window.__MILADY_API_BASE__=${JSON.stringify(initialApiBase)};</script>`;
432+
const script = `<script>window.__MILADY_API_BASE__=${JSON.stringify(initialApiBase)};${initialApiToken ? `Object.defineProperty(window,"__MILADY_API_TOKEN__",{value:${JSON.stringify(initialApiToken)},configurable:true,writable:true,enumerable:false});` : ""}</script>`;
425433
// Inject before </head> if present, otherwise before <body>
426434
if (html.includes("</head>")) {
427435
return html.replace("</head>", `${script}</head>`);
@@ -924,7 +932,7 @@ function injectApiBase(win: BrowserWindow): void {
924932
const agent = getAgentManager();
925933
const port =
926934
agent.getPort() ?? (Number(process.env.MILADY_PORT) || DEFAULT_PORT);
927-
const apiToken = ensureDesktopApiToken();
935+
const apiToken = configureDesktopLocalApiAuth();
928936
pushApiBaseToRenderer(win, `http://127.0.0.1:${port}`, apiToken);
929937
setAgentReady(true);
930938
}
@@ -967,7 +975,7 @@ async function _startAgent(win: BrowserWindow): Promise<void> {
967975
}
968976

969977
const agent = getAgentManager();
970-
const apiToken = ensureDesktopApiToken();
978+
const apiToken = configureDesktopLocalApiAuth();
971979

972980
try {
973981
const status = await agent.start();

apps/app/electrobun/src/native/__tests__/agent.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import path from "node:path";
1010
import { describe, expect, it } from "vitest";
1111

1212
import {
13+
configureDesktopLocalApiAuth,
1314
ensureDesktopApiToken,
1415
getMiladyDistFallbackCandidates,
1516
resolveConfigDir,
@@ -146,3 +147,17 @@ describe("ensureDesktopApiToken", () => {
146147
expect(env.ELIZA_API_TOKEN).toBe(token);
147148
});
148149
});
150+
151+
describe("configureDesktopLocalApiAuth", () => {
152+
it("disables pairing while keeping the mirrored desktop token aliases", () => {
153+
const env: NodeJS.ProcessEnv = {
154+
MILADY_API_TOKEN: "desktop-token",
155+
};
156+
157+
expect(configureDesktopLocalApiAuth(env)).toBe("desktop-token");
158+
expect(env.MILADY_API_TOKEN).toBe("desktop-token");
159+
expect(env.ELIZA_API_TOKEN).toBe("desktop-token");
160+
expect(env.MILADY_PAIRING_DISABLED).toBe("1");
161+
expect(env.ELIZA_PAIRING_DISABLED).toBe("1");
162+
});
163+
});

apps/app/electrobun/src/native/agent.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,7 @@ export function resolveConfigDir(opts?: {
128128
export function ensureDesktopApiToken(
129129
env: NodeJS.ProcessEnv = process.env,
130130
): string {
131-
const existing =
132-
env.MILADY_API_TOKEN?.trim() ?? env.ELIZA_API_TOKEN?.trim() ?? "";
131+
const existing = getDesktopApiToken(env);
133132
if (existing) {
134133
env.MILADY_API_TOKEN = existing;
135134
env.ELIZA_API_TOKEN = existing;
@@ -145,6 +144,35 @@ export function ensureDesktopApiToken(
145144
return generated;
146145
}
147146

147+
export function configureDesktopLocalApiAuth(
148+
env: NodeJS.ProcessEnv = process.env,
149+
): string {
150+
const token = ensureDesktopApiToken(env);
151+
env.MILADY_PAIRING_DISABLED = "1";
152+
env.ELIZA_PAIRING_DISABLED = "1";
153+
return token;
154+
}
155+
156+
function getDesktopApiToken(
157+
env: NodeJS.ProcessEnv = process.env,
158+
): string | null {
159+
const token =
160+
env.MILADY_API_TOKEN?.trim() ?? env.ELIZA_API_TOKEN?.trim() ?? "";
161+
return token || null;
162+
}
163+
164+
function getDesktopApiHeaders(
165+
env: NodeJS.ProcessEnv = process.env,
166+
): Record<string, string> | undefined {
167+
const token = getDesktopApiToken(env);
168+
if (!token) return undefined;
169+
return {
170+
Authorization: `Bearer ${token}`,
171+
"X-Api-Key": token,
172+
"X-Api-Token": token,
173+
};
174+
}
175+
148176
let diagnosticLogPath: string | null = null;
149177

150178
function getDiagnosticLogPath(): string {
@@ -328,12 +356,14 @@ async function waitForHealthy(
328356
timeoutMs: number = getHealthPollTimeoutMs(),
329357
): Promise<boolean> {
330358
const deadline = Date.now() + timeoutMs;
359+
const headers = getDesktopApiHeaders();
331360

332361
while (Date.now() < deadline) {
333362
const port = getPort();
334363
const url = `http://127.0.0.1:${port}/api/health`;
335364
try {
336365
const response = await fetch(url, {
366+
headers,
337367
signal: AbortSignal.timeout(2_000),
338368
});
339369
if (response.ok) {
@@ -542,7 +572,7 @@ export class AgentManager {
542572
throw new Error(reason);
543573
}
544574

545-
ensureDesktopApiToken();
575+
configureDesktopLocalApiAuth();
546576

547577
// Reset per-startup flags
548578
this.pgliteRecoveryDone = false;
@@ -1036,7 +1066,9 @@ export class AgentManager {
10361066
*/
10371067
private async fetchAgentName(port: number): Promise<string> {
10381068
try {
1069+
const headers = getDesktopApiHeaders();
10391070
const response = await fetch(`http://127.0.0.1:${port}/api/agents`, {
1071+
headers,
10401072
signal: AbortSignal.timeout(5_000),
10411073
});
10421074
if (response.ok) {

scripts/electrobun-release-workflow-drift.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,22 @@ describe("Electrobun release workflow drift", () => {
187187
expect(workflow).not.toContain('bun install -g "rcedit@4.0.1"');
188188
});
189189

190+
it("treats auth-protected health probes as valid smoke-test success on every desktop platform", () => {
191+
const windowsScript = fs.readFileSync(WINDOWS_SMOKE_PATH, "utf8");
192+
const macScript = fs.readFileSync(MACOS_SMOKE_SCRIPT_PATH, "utf8");
193+
194+
expect(windowsScript).toContain("function Test-BackendProbeStatus");
195+
expect(windowsScript).toContain(
196+
"return $StatusCode -eq 200 -or $StatusCode -eq 401",
197+
);
198+
expect(windowsScript).toContain("-SkipHttpErrorCheck");
199+
200+
expect(macScript).toContain("backend_health_probe_satisfied()");
201+
expect(macScript).toContain(
202+
'[[ "$status" == "200" || "$status" == "401" ]]',
203+
);
204+
});
205+
190206
it("stages the desktop bundle before restoring local electrobun caches", () => {
191207
const workflow = fs.readFileSync(WORKFLOW_PATH, "utf8");
192208
const stageIndex = workflow.indexOf("name: Stage desktop bundle inputs");

scripts/release-check.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,8 @@ function assertWindowsSmokeScriptHasLeadingParamBlock() {
557557
"Waiting for health endpoint at http://(?:localhost|127\\.0\\.0\\.1):",
558558
"$handler.UseProxy = $false",
559559
'--noproxy "127.0.0.1"',
560+
"function Test-BackendProbeStatus",
561+
"-SkipHttpErrorCheck",
560562
"Dump-PortDiagnostics",
561563
"Dump-ProcessDiagnostics",
562564
"Dump-FailureDiagnostics",
@@ -617,6 +619,8 @@ function assertMacSmokeScriptLaunchesPackagedLauncherDirectly() {
617619
'echo "Packaged renderer asset check PASSED (wrapper archive)."',
618620
'echo "Launcher: $' + "{LAUNCHER_PATH:-<unset>}" + '"',
619621
'local launcher_stdout="$' + "{LAUNCHER_STDOUT:-}" + '"',
622+
"backend_health_probe_satisfied()",
623+
'[[ "$status" == "200" || "$status" == "401" ]]',
620624
"Launcher exited before the first health probe; continuing to wait for packaged app handoff...",
621625
'dump_failure_diagnostics "backend startup log reported a failure"',
622626
'dump_failure_diagnostics "backend never reported a started port"',

0 commit comments

Comments
 (0)