feat(desktop): load runtime self-host config#2012
feat(desktop): load runtime self-host config#2012congvc-dev wants to merge 3 commits intomultica-ai:mainfrom
Conversation
Co-authored-by: multica-agent <github@multica.ai>
|
Someone is attempting to deploy a commit to the IndexLabs Team on Vercel. A member of the Team first needs to authorize it. |
Co-authored-by: multica-agent <github@multica.ai>
Bohan-J
left a comment
There was a problem hiding this comment.
Thanks for the PR — the design and layering look solid (clean shared/main split, fail-closed semantics, schema-versioned JSON, sync-IPC-at-preload-time matches appInfo). Before merge, please address the must-fix below and the first three nits.
Must-fix
1. Add a prod-mode happy-path test for loadRuntimeConfig.
apps/desktop/src/main/runtime-config-loader.test.ts currently covers:
- dev + desktop.json present (env wins)
- prod + missing file (cloud defaults)
- prod + invalid JSON (fail closed)
…but not the core path of this feature: prod + a valid desktop.json → returns the parsed config. parseRuntimeConfig is unit-tested in isolation, but the loader's readFile + parse plumbing isn't directly exercised. Suggested:
it("parses a valid packaged desktop.json", async () => {
const dir = await mkdtemp(join(tmpdir(), "multica-desktop-config-"));
const configPath = join(dir, "desktop.json");
await writeFile(
configPath,
JSON.stringify({ schemaVersion: 1, apiUrl: "https://api.example.com" }),
);
await expect(
loadRuntimeConfig({ isDev: false, configPath, env: {} }),
).resolves.toEqual({
ok: true,
config: {
schemaVersion: 1,
apiUrl: "https://api.example.com",
wsUrl: "wss://api.example.com/ws",
appUrl: "https://api.example.com",
},
});
});Nits to address
2. The APP_URL = "" / WEB_URL = "" fallbacks are confusing dead code.
In apps/desktop/src/renderer/src/platform/navigation.tsx:19-21 and apps/desktop/src/renderer/src/pages/login.tsx:5-7:
const APP_URL = window.desktopAPI.runtimeConfig.ok
? window.desktopAPI.runtimeConfig.config.appUrl
: "";App.tsx:281-292 already short-circuits the entire CoreProvider subtree with <BlockingRuntimeConfigError /> when runtimeConfig.ok === false, so neither navigation.tsx nor login.tsx can ever render with the empty-string fallback active. The : "" branch is unreachable but reads as a legitimate fallback to a future maintainer. Please either:
- Throw with an "invariant violated" message in the
elsebranch, or - Compute these as
runtimeConfig.config.appUrldirectly with a non-null assertion + comment that explains the upstream guard.
Either makes the invariant explicit; the current shape silently encodes "empty APP_URL is an OK runtime state," which it isn't.
3. apps/desktop/.env.production is now orphaned.
The renderer no longer reads import.meta.env.VITE_API_URL / VITE_WS_URL / VITE_APP_URL for these URLs, and the values in .env.production happen to match DEFAULT_RUNTIME_CONFIG exactly, so it's not a behavioral bug — but the file is dead code with a misleading header comment ("electron-vite ... reads this automatically in production mode and inlines the values into the renderer bundle"). The next person who wants to change a Cloud URL will edit this file and wonder why nothing changes. Please either delete the file or rewrite the header to say "kept as historical reference; runtime config now flows through ~/.multica/desktop.json and DEFAULT_RUNTIME_CONFIG in apps/desktop/src/shared/runtime-config.ts."
4. Verify process.env.VITE_* actually reaches main in dev mode.
apps/desktop/src/main/index.ts:198-204 reads dev URLs from process.env.VITE_API_URL / VITE_WS_URL / VITE_APP_URL, but the previous code path used import.meta.env.VITE_* (Vite build-time substitution) in the renderer. These are different sources:
import.meta.env.VITE_*is filled in by Vite from.env.developmentat build time.process.env.VITE_*only contains values that are actually set in the spawned process's environment.
Whether electron-vite propagates .env.development values into the main process's process.env (vs. only into import.meta.env) depends on the toolchain config. If a developer sets VITE_API_URL=http://localhost:8081 in .env.development (without exporting it in their shell), with this PR the main process likely won't see it, fall back to the hard-coded http://localhost:8080, and setTargetApiUrl will hand the daemon a URL that doesn't match the renderer.
Please verify by either:
- Smoke-testing
electron-vite devwith a custom.env.development(VITE_API_URL=http://localhost:8081) and confirming the main process picks it up; or - Switching the main-process side to
import.meta.env.VITE_API_URL(electron-vite supportsimport.meta.envin main as well), which keeps parity with the previous build-time path.
If smoke test passes, please add a one-line note in the loader explaining the assumption (e.g., "electron-vite injects VITE_* from .env.[mode] into process.env for the main process") so future-you doesn't re-question this.
Remaining items from my full review (Windows path string in the blocking error screen, deriveAppUrl reusing the API origin) are informational only — happy to leave those for a follow-up.
Co-authored-by: multica-agent <github@multica.ai>
Bohan-J
left a comment
There was a problem hiding this comment.
Re-reviewed b48b167 — all four asks addressed cleanly. CI green (backend + frontend). Approving.
1. Prod happy-path test — done. runtime-config-loader.test.ts now exercises the readFile + parseRuntimeConfig chain end-to-end. Matches the suggestion verbatim.
2. APP_URL fallback — done. Replaced the : "" dead branch with requireRuntimeAppUrl(scope) that throws an "Invariant violated" error if called when runtimeConfig.ok === false. Three callsites (DesktopNavigationProvider, TabNavigationProvider, DesktopLoginPage) all guarded; appUrl correctly added to the useMemo deps in both navigation providers. The invariant is upheld by App.tsx's runtimeConfigResult.ok ? gate, so the throw is unreachable in practice but loudly explicit if it ever isn't.
3. .env.production — deleted. No more orphaned file; Cloud defaults live solely in DEFAULT_RUNTIME_CONFIG.
4. Dev env source — switched to import.meta.env. apps/desktop/src/main/index.ts:196-213 now reads import.meta.env.VITE_* (electron-vite's build-time substitution path) instead of process.env.VITE_*, with a comment explaining why. This keeps parity with the renderer's previous behavior and removes the propagation-via-process.env uncertainty.
Nothing else to flag — design, layering, fail-closed semantics, and IPC ordering were already solid. Ready to merge.
What does this PR do?
Adds a runtime self-host configuration path for the Electron desktop app so packaged Desktop builds can point at a self-hosted Multica instance without rebuilding the app or hand-editing
VITE_*build-time env.The source of truth is the code changelist: packaged Desktop now reads
~/.multica/desktop.jsonbefore renderer startup. If the file is missing, Desktop keeps the Cloud defaults. If the file exists but is invalid, Desktop fails closed with a blocking config error instead of silently falling back to Cloud.Related Issue
Closes #1371
Refs #1768
Refs #1777
Type of Change
Changes Made
apps/desktop/src/shared/runtime-config.ts~/.multica/desktop.json.schemaVersion: 1andapiUrl.wsUrlandappUrl; derives them fromapiUrlwhen omitted.apps/desktop/src/main/runtime-config-loader.tselectron-vite devenv precedence forVITE_API_URL/VITE_WS_URL/VITE_APP_URL.apps/desktop/src/main/index.ts,apps/desktop/src/preload/index.ts,apps/desktop/src/preload/index.d.tsapps/desktop/src/renderer/src/App.tsx,apps/desktop/src/renderer/src/pages/login.tsx,apps/desktop/src/renderer/src/platform/navigation.tsxapiUrl,wsUrl, andappUrlbefore the first API/client request.desktop.jsonis invalid.apps/desktop/src/shared/runtime-config.test.ts,apps/desktop/src/main/runtime-config-loader.test.tsapps/docs/content/docs/desktop-app.mdx,apps/docs/content/docs/desktop-app.zh.mdx~/.multica/desktop.jsonself-host path, required/optional fields, derived URL behavior, invalid-config failure mode, and dev-env precedence.apps/docs/content/docs/self-host-quickstart.mdx,apps/docs/content/docs/self-host-quickstart.zh.mdxRuntime config format
Minimal self-host config:
{ "schemaVersion": 1, "apiUrl": "https://api.your-domain" }Optional explicit URL config:
{ "schemaVersion": 1, "apiUrl": "https://api.your-domain", "wsUrl": "wss://api.your-domain/ws", "appUrl": "https://your-domain" }apiUrlmust usehttporhttps;wsUrlmust usewsorwss. Search params and hashes are stripped during normalization, and trailing slashes are trimmed.How to Test
pnpm --filter @multica/desktop typecheck pnpm --filter @multica/desktop testhttps://congvc-x99.taila6fa8a.ts.net:18443wss://congvc-x99.taila6fa8a.ts.net:18443/wshttps://congvc-x99.taila6fa8a.ts.net:18443~/.multica/desktop.jsonand relaunch Desktop. Confirm it shows a blocking runtime config error instead of Cloud login.~/.multica/desktop.jsonand relaunch Desktop. Confirm existing Cloud/default behavior still works.Local verification run:
pnpm --filter @multica/desktop typecheck✅pnpm --filter @multica/desktop test✅ — 8 files / 89 testspnpm --filter @multica/docs typecheck✅Linux note: release-quality
.dmg/.zippackaging still requires a macOS runner for Electron mac targets/signing/notarization.Checklist
AI Disclosure
AI tool used: Multica Agent / Hermes Driver Plane, with Codex implementation and documentation handoff in the Multica workspace.
Prompt / approach:
Implemented from the accepted Multica workspace design thread and then corrected against the actual PR #2012 code changelist as source truth: define
~/.multica/desktop.json, parse/validate in the main process, expose through preload before renderer startup, replace baked Desktop URLs with resolved runtime config, document the runtime config path, and verify with targeted desktop/docs checks.Screenshots (optional)
Not included. UI change is limited to a blocking runtime config error screen for invalid local config; primary behavior is startup/runtime configuration wiring.