Skip to content
This repository was archived by the owner on Jun 8, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 22 additions & 8 deletions apps/desktop/src/main/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { seedReposConfig } from "./seed-repos-config.js";
import { ActivityLogStore } from "./activity-log-store.js";
import { ApprovalStore } from "./approval-store.js";
import type { GatewayApprovalRequest, GatewayApprovalResult } from "../server/router.js";
import { normalizeAndValidateApiOrigin, normalizeWebAppOrigin } from "./origin-policy.js";
import { normalizeAndValidateOrigin, normalizeWebAppOrigin } from "./origin-policy.js";
import { LocalSessionStore } from "./local-session-store.js";
import pkg from "electron-updater";
const { autoUpdater } = pkg;
Expand Down Expand Up @@ -97,7 +97,8 @@ export class DesktopApplication {
() => this.getSymphonyDir(),
this.sessionStore,
() => this.apiKeyStore.getApiKey(),
() => this.settingsStore.getApiOrigin()
() => this.settingsStore.getApiOrigin(),
() => this.settingsStore.getWebAppOrigin()
);
this.commandExecutor = new CloudCommandExecutor({
getGatewayPort: () => this.server.getActivePort(),
Expand All @@ -117,7 +118,7 @@ export class DesktopApplication {
}
});
this.cloudSocket = new CloudSocketService({
getApiOrigin: () => this.settingsStore.getApiOrigin(),
getRelayOrigin: () => this.settingsStore.getRelayOrigin(),
getApiKey: () => this.apiKeyStore.getApiKey(),
getAllowedDirectories: () => this.getAllowedDirectoriesFromSandbox(),
getMaxInFlightCommands: () => MAX_IN_FLIGHT_COMMANDS,
Expand Down Expand Up @@ -200,11 +201,12 @@ export class DesktopApplication {
try {
await this.server.start();
const configuredOrigins = {
relayOrigin: this.settingsStore.getRelayOrigin(),
apiOrigin: this.settingsStore.getApiOrigin(),
webAppOrigin: this.settingsStore.getWebAppOrigin()
};
this.refreshTrayState(
`Serving on localhost:${this.server.getActivePort()} | api=${configuredOrigins.apiOrigin} web=${configuredOrigins.webAppOrigin}`
`Serving on localhost:${this.server.getActivePort()} | relay=${configuredOrigins.relayOrigin} api=${configuredOrigins.apiOrigin} web=${configuredOrigins.webAppOrigin}`
);

if (this.cloudConnectionEnabled) {
Expand Down Expand Up @@ -654,15 +656,19 @@ export class DesktopApplication {
async (_event, partial: {
sandboxBaseDirectory?: string;
onboardingCompleted?: boolean;
relayOrigin?: string;
apiOrigin?: string;
webAppOrigin?: string;
defaultApprovalTier?: "auto" | "low" | "medium" | "high";
autoApprovalRules?: Record<string, "auto" | "low" | "medium" | "high">;
}) => {
const currentSettings = this.settingsStore.getAll();
const nextPartial = { ...partial };
if (typeof partial.relayOrigin === "string") {
nextPartial.relayOrigin = normalizeAndValidateOrigin(partial.relayOrigin);
}
if (typeof partial.apiOrigin === "string") {
nextPartial.apiOrigin = normalizeAndValidateApiOrigin(partial.apiOrigin);
nextPartial.apiOrigin = normalizeAndValidateOrigin(partial.apiOrigin);
}
if (typeof partial.webAppOrigin === "string") {
nextPartial.webAppOrigin = normalizeWebAppOrigin(partial.webAppOrigin);
Expand Down Expand Up @@ -702,6 +708,7 @@ export class DesktopApplication {
ipcMain.handle("desktop:get-runtime-status", () => ({
port: this.server.getActivePort(),
cloudStatus: this.cloudStatus,
relayOrigin: this.settingsStore.getRelayOrigin(),
apiOrigin: this.settingsStore.getApiOrigin(),
sandboxBaseDirectory: this.settingsStore.getSandboxBaseDirectory(),
commandsPaused: this.cloudCommandsPaused,
Expand Down Expand Up @@ -794,13 +801,19 @@ export class DesktopApplication {
async (
_event,
payload: {
apiOrigin: string;
relayOrigin?: string;
apiOrigin?: string;
webAppOrigin: string;
sandboxBaseDirectory: string;
apiKey?: string;
}
) => {
const apiOrigin = normalizeAndValidateApiOrigin(payload.apiOrigin);
const relayOrigin = typeof payload.relayOrigin === "string" && payload.relayOrigin.trim()
? normalizeAndValidateOrigin(payload.relayOrigin)
: undefined;
const apiOrigin = typeof payload.apiOrigin === "string" && payload.apiOrigin.trim()
? normalizeAndValidateOrigin(payload.apiOrigin)
: undefined;
const webAppOrigin = normalizeWebAppOrigin(payload.webAppOrigin);
const sandboxBaseDirectory = normalizeScopePath(payload.sandboxBaseDirectory);
if (!sandboxBaseDirectory) {
Expand All @@ -816,7 +829,8 @@ export class DesktopApplication {
}

this.settingsStore.update({
apiOrigin,
...(relayOrigin !== undefined ? { relayOrigin } : {}),
...(apiOrigin !== undefined ? { apiOrigin } : {}),
webAppOrigin,
sandboxBaseDirectory,
onboardingCompleted: true
Expand Down
16 changes: 8 additions & 8 deletions apps/desktop/src/main/cloud-socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import {
type DesktopHelloEvent,
type DesktopPresenceEvent
} from "./cloud-protocol.js";
import { normalizeAndValidateApiOrigin } from "./origin-policy.js";
import { normalizeAndValidateOrigin } from "./origin-policy.js";

export interface CloudSocketOptions {
getApiOrigin: () => string;
getRelayOrigin: () => string;
getApiKey: () => string | null;
getAllowedDirectories: () => string[];
getMaxInFlightCommands: () => number;
Expand Down Expand Up @@ -55,17 +55,17 @@ export class CloudSocketService {
return;
}

let apiOrigin: string;
let relayOrigin: string;
try {
apiOrigin = normalizeAndValidateApiOrigin(this.options.getApiOrigin());
relayOrigin = normalizeAndValidateOrigin(this.options.getRelayOrigin());
} catch (error) {
const message = error instanceof Error ? error.message : "invalid API origin";
const message = error instanceof Error ? error.message : "invalid relay origin";
this.notifyStatus({ state: "degraded", error: message });
return;
}

this.notifyStatus({ state: "idle" });
this.connect(apiKey, apiOrigin);
this.connect(apiKey, relayOrigin);
Comment thread
shafty023 marked this conversation as resolved.
}

stop(): void {
Expand Down Expand Up @@ -111,8 +111,8 @@ export class CloudSocketService {
}
}

private connect(apiKey: string, apiOrigin: string): void {
const socket = io(`${apiOrigin}/desktop-gateway`, {
private connect(apiKey: string, relayOrigin: string): void {
const socket = io(`${relayOrigin}/desktop-gateway`, {
transports: ["websocket"],
reconnection: true,
reconnectionDelay: 1000,
Expand Down
11 changes: 7 additions & 4 deletions apps/desktop/src/main/origin-policy.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);

export function normalizeAndValidateApiOrigin(rawOrigin: string): string {
export function normalizeAndValidateOrigin(rawOrigin: string): string {
const trimmed = rawOrigin.trim();
if (!trimmed) {
throw new Error("API origin is required");
throw new Error("Origin is required");
}

let parsed: URL;
try {
parsed = new URL(trimmed);
} catch {
throw new Error("API origin must be a valid URL");
throw new Error("Origin must be a valid URL");
}

if (parsed.protocol === "https:") {
Expand All @@ -22,10 +22,13 @@ export function normalizeAndValidateApiOrigin(rawOrigin: string): string {
}

throw new Error(
"API origin must use https (http is allowed only for localhost/127.0.0.1 in local development)"
"Origin must use https (http is allowed only for localhost/127.0.0.1 in local development)"
);
}

/** @deprecated Use normalizeAndValidateOrigin instead. */
export const normalizeAndValidateApiOrigin = normalizeAndValidateOrigin;

export function normalizeWebAppOrigin(rawOrigin: string): string {
const trimmed = rawOrigin.trim();
if (!trimmed) {
Expand Down
54 changes: 52 additions & 2 deletions apps/desktop/src/main/settings-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type DesktopSettings,
type RiskTier
} from "../shared/contracts.js";
import { normalizeAndValidateOrigin } from "./origin-policy.js";

export interface SettingsStoreOptions {
cwd?: string;
Expand All @@ -17,8 +18,7 @@ export class SettingsStore {
constructor(options?: SettingsStoreOptions) {
this.store = new Store<DesktopSettings>({
name: options?.name ?? "desktop-settings",
cwd: options?.cwd,
defaults: DEFAULT_DESKTOP_SETTINGS
cwd: options?.cwd
});

// Migration: delete stale allowedDirectories key from previous versions.
Expand All @@ -27,12 +27,55 @@ export class SettingsStore {
if ("allowedDirectories" in this.store.store) {
this.store.delete("allowedDirectories" as keyof DesktopSettings);
}

// Migration: rename apiOrigin → relayOrigin, preserve authApiOrigin → apiOrigin.
// With defaults removed, this.store.store only contains actually-persisted keys,
// so key-presence checks are reliable.
const raw = this.store.store as unknown as Record<string, unknown>;
const hadRelayOrigin = "relayOrigin" in raw;
const hadAuthApiOrigin = "authApiOrigin" in raw;
const oldApiOrigin = raw.apiOrigin as string | undefined;
const oldAuthApiOrigin = raw.authApiOrigin as string | undefined;

if (!hadRelayOrigin && typeof oldApiOrigin === "string") {
// Legacy: apiOrigin held the relay URL. Move it to relayOrigin.
let relayOrigin = DEFAULT_DESKTOP_SETTINGS.relayOrigin;
try {
relayOrigin = normalizeAndValidateOrigin(oldApiOrigin);
} catch {
// Fall back to default on invalid value
}
this.store.set("relayOrigin" as keyof DesktopSettings, relayOrigin);

if (hadAuthApiOrigin && typeof oldAuthApiOrigin === "string") {
// Intermediate build: authApiOrigin held the REST API URL. Promote it.
let apiOrigin = DEFAULT_DESKTOP_SETTINGS.apiOrigin;
try {
apiOrigin = normalizeAndValidateOrigin(oldAuthApiOrigin);
} catch {
// Fall back to default on invalid value
}
this.store.set("apiOrigin" as keyof DesktopSettings, apiOrigin);
} else {
// Pre-auth install: no REST API origin was ever set. Use default.
this.store.set("apiOrigin" as keyof DesktopSettings, DEFAULT_DESKTOP_SETTINGS.apiOrigin);
}
}

// Always clean up stale authApiOrigin key (intermediate build artifact).
if (hadAuthApiOrigin) {
this.store.delete("authApiOrigin" as keyof DesktopSettings);
}
}

getAll(): DesktopSettings {
return { ...DEFAULT_DESKTOP_SETTINGS, ...this.store.store };
}

getRelayOrigin(): string {
return this.store.get("relayOrigin" as keyof DesktopSettings, DEFAULT_DESKTOP_SETTINGS.relayOrigin) as string;
}

getApiOrigin(): string {
return this.store.get("apiOrigin", DEFAULT_DESKTOP_SETTINGS.apiOrigin);
}
Expand Down Expand Up @@ -81,6 +124,10 @@ export class SettingsStore {
this.store.set("defaultApprovalTier", defaultApprovalTier);
}

setRelayOrigin(relayOrigin: string): void {
this.store.set("relayOrigin" as keyof DesktopSettings, relayOrigin);
}

setApiOrigin(apiOrigin: string): void {
this.store.set("apiOrigin", apiOrigin);
}
Expand Down Expand Up @@ -116,6 +163,9 @@ export class SettingsStore {
if (typeof partial.cloudConnectionEnabled === "boolean") {
this.store.set("cloudConnectionEnabled", partial.cloudConnectionEnabled);
}
if (typeof partial.relayOrigin === "string") {
this.store.set("relayOrigin" as keyof DesktopSettings, partial.relayOrigin);
}
if (typeof partial.apiOrigin === "string") {
this.store.set("apiOrigin", partial.apiOrigin);
}
Expand Down
Loading
Loading