diff --git a/main/backend-sidecar.ts b/main/backend-sidecar.ts index 8a824e8..ccaf6d3 100644 --- a/main/backend-sidecar.ts +++ b/main/backend-sidecar.ts @@ -1,6 +1,6 @@ import { spawn, type ChildProcess } from "node:child_process"; import { once } from "node:events"; -import { existsSync } from "node:fs"; +import { existsSync, mkdirSync } from "node:fs"; import { randomUUID } from "node:crypto"; import { createServer } from "node:net"; import { homedir } from "node:os"; @@ -250,12 +250,14 @@ export function createBackendSidecarController(options: CreateBackendSidecarCont const url = `http://${SIDE_CAR_HOST}:${port}`; const dataDir = path.join(options.app.getPath("userData"), "backend"); - const workingDirectory = options.app.isPackaged - ? path.dirname(entrypoint) - : options.app.getAppPath(); + // Keep the managed backend's process cwd inside OpenGUI-owned app data. + // Using the app/source directory as cwd can make macOS show privacy prompts + // (for example “OpenGUI.app wants access to Documents”) when the app is run + // from a protected user folder, even though the backend only needs its data dir. + mkdirSync(dataDir, { recursive: true }); const child = spawn(runtime.command, runtime.args, { - cwd: workingDirectory, + cwd: dataDir, env: { ...runtime.env, HOST: SIDE_CAR_HOST, @@ -265,6 +267,7 @@ export function createBackendSidecarController(options: CreateBackendSidecarCont OPENGUI_DATA_DIR: dataDir, OPENGUI_CORS_ORIGIN: process.env.OPENGUI_CORS_ORIGIN || "*", OPENGUI_MODE: "desktop-sidecar", + OPENGUI_SERVER_MODE: "backend-only", }, stdio: ["ignore", "pipe", "pipe"], }); diff --git a/server/harness-inventory.ts b/server/harness-inventory.ts index ef5fe42..d1fc39e 100644 --- a/server/harness-inventory.ts +++ b/server/harness-inventory.ts @@ -21,6 +21,13 @@ const LABEL_BY_HARNESS: Record = { const HARNESS_IDS: HarnessId[] = ["opencode", "claude-code", "pi", "codex"]; +function safeDiagnosticCwd() { + // Version/path probes do not need a project cwd. Avoid inheriting a cwd inside + // Documents/Desktop in dev or ad-hoc packaged launches, which can trigger + // macOS privacy prompts before the user has opened a project. + return homedir(); +} + export function isHarnessId(value: unknown): value is HarnessId { return typeof value === "string" && HARNESS_IDS.includes(value as HarnessId); } @@ -48,6 +55,7 @@ function commandFromShell(command: string): string | null { if (process.platform === "win32") return null; for (const shell of [process.env.SHELL, "/bin/zsh", "/bin/bash"].filter(Boolean) as string[]) { const result = spawnSync(shell, ["-lc", `command -v ${command}`], { + cwd: safeDiagnosticCwd(), encoding: "utf8", timeout: 3000, stdio: ["ignore", "pipe", "ignore"], @@ -87,6 +95,7 @@ export function resolveHarnessCli(harnessId: HarnessId): HarnessInventoryCliDiag function readVersion(resolvedPath: string): string | null { const result = spawnSync(resolvedPath, ["--version"], { + cwd: safeDiagnosticCwd(), encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "pipe"], diff --git a/src/hooks/use-agent-impl-core.tsx b/src/hooks/use-agent-impl-core.tsx index 8f765b9..885d159 100644 --- a/src/hooks/use-agent-impl-core.tsx +++ b/src/hooks/use-agent-impl-core.tsx @@ -894,20 +894,7 @@ function InternalAgentProvider({ }); }), ); - - const defaultChatDirectory = stateRef.current.defaultChatDirectory; - if (defaultChatDirectory && !detachedProject) { - await ensureDirectoryConnection(defaultChatDirectory, { transient: true }); - } - }, [addProject, detachedProject, discoveryBackendIds, ensureDirectoryConnection, openGuiClient]); - - const ensureDefaultChatConnection = useCallback(async () => { - const defaultChatDirectory = stateRef.current.defaultChatDirectory; - if (!defaultChatDirectory || detachedProject) return; - await ensureDirectoryConnection(defaultChatDirectory, { - transient: true, - }); - }, [detachedProject, ensureDirectoryConnection]); + }, [addProject, discoveryBackendIds, openGuiClient]); const removeProject = useCallback( async (directory: string) => { @@ -1169,17 +1156,6 @@ function InternalAgentProvider({ shellWorkspacePolicy.localWorkspaceMode, ]); - useEffect(() => { - if (allBackends.length === 0 || detachedProject) return; - if (!state.defaultChatDirectory) return; - void ensureDefaultChatConnection(); - }, [ - allBackends.length, - detachedProject, - ensureDefaultChatConnection, - state.defaultChatDirectory, - ]); - useEffect(() => { if (detachedProject) return; if (state.activeSessionId || state.activeTargetDirectory) return; @@ -2151,9 +2127,11 @@ function InternalAgentProvider({ const startNewChat = useCallback(async () => { const defaultChatDirectory = normalizeProjectPath(stateRef.current.defaultChatDirectory ?? ""); if (!defaultChatDirectory) return; + // Opening a blank chat should not touch the filesystem. The project connection is + // created lazily when the user sends a prompt or explicitly connects a project, + // which avoids unnecessary macOS Documents/Desktop permission prompts. setActiveTarget(defaultChatDirectory, preferredBackendId); - await ensureDirectoryConnection(defaultChatDirectory, { transient: true }); - }, [ensureDirectoryConnection, preferredBackendId, setActiveTarget]); + }, [preferredBackendId, setActiveTarget]); const setActiveTargetDirectory = useCallback( (directory: string) => {