From da04b4cbbe03e573485807c59de0f90529d75eb8 Mon Sep 17 00:00:00 2001 From: Epinephrine Date: Wed, 14 Oct 2026 23:16:00 -0700 Subject: [PATCH 1/5] fix(startup): launch Desktop by its registered app bundle path Launch Desktop from the bundle path recorded in its login registration, with matching installation ownership and fail-closed manual-open guidance. Preserve the app-bundle suffix refusal and explicit ownership fixture types. Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- src/cli/ensure-desktop-cold-start.ts | 61 +++++++++- structure/gui-and-management-api.md | 2 +- .../cli/cli-ensure-desktop-cold-start.test.ts | 109 +++++++++++++++++- 3 files changed, 160 insertions(+), 12 deletions(-) diff --git a/src/cli/ensure-desktop-cold-start.ts b/src/cli/ensure-desktop-cold-start.ts index dd7a94de2c4..43ceeffab2d 100644 --- a/src/cli/ensure-desktop-cold-start.ts +++ b/src/cli/ensure-desktop-cold-start.ts @@ -10,8 +10,9 @@ * * Rules, in order: * - A running Desktop app is the supervisor. Wait for its proxy; never start a competing one. - * - A durable Desktop claim with no running app: start Desktop (hidden, as a login launch would) - * where that is possible, then wait. Elsewhere, fail with guidance. A missing Desktop process + * - A durable Desktop claim with no running app: start Desktop (hidden, by the path its own + * login registration recorded — LaunchServices never picks the bundle) where that is + * verifiable, then wait. Elsewhere, fail with guidance. A missing Desktop process * never authorizes a CLI proxy by itself; handing ownership to the CLI stays an explicit * `ocx service install` consent step. * - Ownership or the process list that cannot be read is not evidence of absence: refuse. @@ -22,7 +23,7 @@ */ import { execFileSync } from "node:child_process"; import { randomUUID } from "node:crypto"; -import { existsSync, linkSync, readdirSync, readFileSync, readlinkSync, statSync, unlinkSync, writeFileSync } from "node:fs"; +import { accessSync, constants, existsSync, linkSync, readdirSync, readFileSync, readlinkSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { basename, dirname, join, win32 } from "node:path"; import { getConfigDir } from "../config/paths"; @@ -220,11 +221,59 @@ export function inspectDesktopProcess(options: DesktopProcessOptions = {}): Desk } } -/** Start the macOS app hidden, the same way its login item does. */ -export function launchDesktopApp(options: { platform?: NodeJS.Platform; run?: Execute } = {}): boolean { +const DESKTOP_BUNDLE_ID = "com.opencodex.desktop"; +const DESKTOP_PLIST_LABEL = "OpenCodex"; + +/** + * The app bundle the consented installation itself registered as its Start-at-Login program — + * not whichever bundle LaunchServices would resolve for our identifier. `open -b` answers a + * mutable per-user registry: any planted bundle claiming {@link DESKTOP_BUNDLE_ID} could be + * selected and executed in the real app's place. The recorded claim, this app's own install-id + * store, and the login registration it wrote are the durable evidence of which installation + * that is; a claim whose registration is absent, malformed, stale or points off a Desktop + * executable refuses the launch instead of guessing. + */ +function consentedDesktopBundle(home: string, installId: string, run: Execute): string | null { + const localId = readFileSync( + join(home, "Library", "Application Support", DESKTOP_BUNDLE_ID, "install-id"), "utf8", + ).trim(); + if (!localId || localId !== installId) return null; + const plistPath = realpathSync(join(home, "Library", "LaunchAgents", `${DESKTOP_PLIST_LABEL}.plist`)); + const registration = JSON.parse( + run("/usr/bin/plutil", ["-convert", "json", "-o", "-", plistPath], 3_000), + ) as { Label?: unknown; ProgramArguments?: unknown; Program?: unknown }; + const args = registration.ProgramArguments; + // The shapes auto-launch writes: [program] or [program, "--autostart"]; see the migration + // rules in desktop/src-tauri/src/login_registration.rs, which classify anything else Leave. + if (registration.Label !== DESKTOP_PLIST_LABEL || !Array.isArray(args) + || (args.length !== 1 && args.length !== 2) || typeof args[0] !== "string" + || (args.length === 2 && args[1] !== "--autostart") + || (registration.Program !== undefined && registration.Program !== args[0])) return null; + const executable = realpathSync(args[0]); + if (!isDesktopExecutable(executable, "darwin")) return null; + accessSync(executable, constants.X_OK); + const bundle = dirname(dirname(dirname(executable))); + if (!bundle.endsWith(".app")) return null; + const info = JSON.parse( + run("/usr/bin/plutil", ["-convert", "json", "-o", "-", join(bundle, "Contents", "Info.plist")], 3_000), + ) as { CFBundleIdentifier?: unknown }; + return info.CFBundleIdentifier === DESKTOP_BUNDLE_ID ? bundle : null; +} + +/** Start the consented macOS app hidden, by the path its own login registration recorded. */ +export function launchDesktopApp(options: { + platform?: NodeJS.Platform; + run?: Execute; + home?: string; + ownership?: () => ReturnType; +} = {}): boolean { if ((options.platform ?? process.platform) !== "darwin") return false; try { - (options.run ?? execute)("/usr/bin/open", ["-g", "-b", "com.opencodex.desktop", "--args", "--autostart"], 10_000); + const owner = (options.ownership ?? resolveServiceOwnership)(); + if (owner.kind !== "owned" || owner.ownership.owner !== "desktop") return false; + const bundle = consentedDesktopBundle(options.home ?? homedir(), owner.ownership.installId, options.run ?? execute); + if (bundle === null) return false; + (options.run ?? execute)("/usr/bin/open", ["-g", bundle, "--args", "--autostart"], 10_000); return true; } catch { return false; } } diff --git a/structure/gui-and-management-api.md b/structure/gui-and-management-api.md index 961a55f07ed..61282cf2c13 100644 --- a/structure/gui-and-management-api.md +++ b/structure/gui-and-management-api.md @@ -499,7 +499,7 @@ unvalidated Bun builds is unchanged (`src/lib/bun-stream-caps.ts`). ## Startup safety -Startup safety credits macOS and Linux `desktop` protection only after matching the durable app ownership or live supervision, an enabled login registration (macOS: loaded in this session or registered for the next login; Linux: enabled XDG autostart entry), and live app-to-bundled-proxy process relationship. This does not claim an independently installed CLI service. Missing or stale evidence remains at risk. The durable desktop claim remains visible when identity, login registration, or supervision fails; service install and repair controls and their copyable command stay disabled while the app owns or supervises the runtime. Shim controls stay enabled because `ocx ensure` (`src/cli/ensure-desktop-cold-start.ts`) waits for a running Desktop (matched by executable identity), relaunches a claimed macOS Desktop under a pid-held launch lease, and otherwise fails rather than starting a CLI proxy, including when ownership or the process list cannot be read. Under Desktop the shim installer child gets the login-shell PATH (`src/server/login-shell-path.ts`); an intact overlay not active on the dashboard's PATH reports `shimActivationPending` and shows the activation step instead of Repair. The Desktop row says "Running · Start at Login not verified" (`desktopBadgeKey` in `gui/src/startup-health-ui.ts`) for a fresh reading of a running owned or supervised app without verified login registration, and "Installed but unhealthy" otherwise; failed or stale readings never get the narrower label. Recovery guidance asks the user to check Start at Login and, when it is already checked, the system's login-item allowance. Compiled startup probes and the dashboard installer child (`runCliInstall` in `src/server/startup-action-control.ts`) use the standalone-aware self-launch argument builder. +Startup safety credits macOS and Linux `desktop` protection only after matching the durable app ownership or live supervision, an enabled login registration (macOS: loaded in this session or registered for the next login; Linux: enabled XDG autostart entry), and live app-to-bundled-proxy process relationship. This does not claim an independently installed CLI service. Missing or stale evidence remains at risk. The durable desktop claim remains visible when identity, login registration, or supervision fails; service install and repair controls and their copyable command stay disabled while the app owns or supervises the runtime. Shim controls stay enabled because `ocx ensure` (`src/cli/ensure-desktop-cold-start.ts`) waits for a running Desktop (matched by executable identity), relaunches a claimed macOS Desktop under a pid-held launch lease, and otherwise fails rather than starting a CLI proxy, including when ownership or the process list cannot be read. The relaunch runs the path the consented installation's own login registration recorded, after the app-local install id is checked against the recorded claim and the resolved bundle path ends in `.app`; a bundle-identifier lookup never chooses the launch target, so a claim whose registration or id evidence cannot be verified is refused rather than guessed. Under Desktop the shim installer child gets the login-shell PATH (`src/server/login-shell-path.ts`); an intact overlay not active on the dashboard's PATH reports `shimActivationPending` and shows the activation step instead of Repair. The Desktop row says "Running · Start at Login not verified" (`desktopBadgeKey` in `gui/src/startup-health-ui.ts`) for a fresh reading of a running owned or supervised app without verified login registration, and "Installed but unhealthy" otherwise; failed or stale readings never get the narrower label. Recovery guidance asks the user to check Start at Login and, when it is already checked, the system's login-item allowance. Compiled startup probes and the dashboard installer child (`runCliInstall` in `src/server/startup-action-control.ts`) use the standalone-aware self-launch argument builder. **Startup safety** is reachable by route (`/#startup`) and rendered by the app, but it is not a sidebar entry: it is entered from the dashboard's startup-state row, which links there whether the diff --git a/tests/cli/cli-ensure-desktop-cold-start.test.ts b/tests/cli/cli-ensure-desktop-cold-start.test.ts index 0e0289ce2e1..94c397b3f2f 100644 --- a/tests/cli/cli-ensure-desktop-cold-start.test.ts +++ b/tests/cli/cli-ensure-desktop-cold-start.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { mkdirSync, mkdtempSync, readdirSync, readFileSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs"; +import { chmodSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { @@ -15,6 +15,7 @@ import { type DesktopOwnershipState, type DesktopProcessState, } from "../../src/cli/ensure-desktop-cold-start"; +import type { ServiceOwnershipResolution } from "../../src/service/state"; import { removeTreeWithRetry } from "../helpers/remove-tree"; interface Calls { launches: number; waits: number[]; released: number } @@ -158,11 +159,109 @@ describe("Desktop process detection by executable identity", () => { expect(inspectDesktopProcess({ platform: "win32", env, run: () => { throw new Error("denied"); } })).toBe("unknown"); }); - test("only macOS launches Desktop, hidden, through its bundle identifier", () => { + const owned = { + kind: "owned" as const, + ownership: { owner: "desktop" as const, installId: "install-a", consentGeneration: 1 }, + revision: 3, + }; + /** A fake consented install: app-local install-id, a LaunchAgent plist, and a bundle on disk. */ + function fakeDesktopHome(options: { + installId?: string; args?: string[]; bareArgs?: boolean; label?: string; identifier?: string; + plist?: "present" | "absent"; executable?: "present" | "absent"; bundleName?: string; + } = {}) { + const home = mkdtempSync(join(tmpdir(), "ocx-desktop-home-")); + const bundle = join(home, "Applications", options.bundleName ?? "OpenCodex.app"); + const executable = join(bundle, "Contents", "MacOS", "opencodex-desktop"); + mkdirSync(join(bundle, "Contents", "MacOS"), { recursive: true }); + if (options.executable !== "absent") { writeFileSync(executable, ""); chmodSync(executable, 0o755); } + writeFileSync(join(bundle, "Contents", "Info.plist"), ""); + const store = join(home, "Library", "Application Support", "com.opencodex.desktop"); + mkdirSync(store, { recursive: true }); + if (options.installId !== undefined) writeFileSync(join(store, "install-id"), `${options.installId}\n`); + mkdirSync(join(home, "Library", "LaunchAgents"), { recursive: true }); + if (options.plist !== "absent") writeFileSync(join(home, "Library", "LaunchAgents", "OpenCodex.plist"), ""); const seen: string[][] = []; - expect(launchDesktopApp({ platform: "darwin", run: (command, args) => { seen.push([command, ...args]); return ""; } })).toBe(true); - expect(seen).toEqual([["/usr/bin/open", "-g", "-b", "com.opencodex.desktop", "--args", "--autostart"]]); - expect(launchDesktopApp({ platform: "darwin", run: () => { throw new Error("no app"); } })).toBe(false); + const run = (command: string, args: string[]) => { + seen.push([command, ...args]); + const target = args[args.length - 1] ?? ""; + if (command !== "/usr/bin/plutil") return ""; + if (target.endsWith(`${"OpenCodex"}.plist`)) { + return JSON.stringify({ + Label: options.label ?? "OpenCodex", + ProgramArguments: options.bareArgs ? [executable] : (options.args ?? [executable, "--autostart"]), + }); + } + if (target.endsWith("Info.plist")) return JSON.stringify({ CFBundleIdentifier: options.identifier ?? "com.opencodex.desktop" }); + return ""; + }; + return { home, bundle, seen, run }; + } + const launch = (fixture: ReturnType, ownership: () => ServiceOwnershipResolution = () => owned) => + launchDesktopApp({ platform: "darwin", home: fixture.home, ownership, run: fixture.run }); + + test("a Desktop claim launches the app its own login registration recorded, never a bundle-id lookup", () => { + const fixture = fakeDesktopHome({ installId: "install-a" }); + try { + expect(launch(fixture)).toBe(true); + expect(fixture.seen.at(-1)).toEqual(["/usr/bin/open", "-g", realpathSync(fixture.bundle), "--args", "--autostart"]); + expect(fixture.seen.flat().includes("-b")).toBe(false); + } finally { removeTreeWithRetry(fixture.home); } + }); + + test("the registration shape auto-launch writes without --autostart is still the consented path", () => { + const fixture = fakeDesktopHome({ installId: "install-a", bareArgs: true }); + try { + expect(launch(fixture)).toBe(true); + expect(fixture.seen.at(-1)).toEqual(["/usr/bin/open", "-g", realpathSync(fixture.bundle), "--args", "--autostart"]); + } finally { removeTreeWithRetry(fixture.home); } + }); + + test("a non-app directory refuses the launch without reading its Info.plist or waiting for a proxy", async () => { + for (const bundleName of ["OpenCodex", "OpenCodex.app.backup"]) { + const fixture = fakeDesktopHome({ installId: "install-a", bundleName }); + try { + const calls = fresh(); + const dependencies = deps({ ownership: "desktop", process: "absent" }, calls); + dependencies.launchDesktop = () => launch(fixture); + const result = await runDesktopColdStart(dependencies); + expect(result.kind === "failed" && result.message).toContain("Open OpenCodex manually"); + expect(calls.waits).toEqual([]); + expect(calls.released).toBe(1); + expect(fixture.seen).toHaveLength(1); + expect(fixture.seen[0]?.[0]).toBe("/usr/bin/plutil"); + expect(fixture.seen[0]?.at(-1)).toEndWith("OpenCodex.plist"); + } finally { removeTreeWithRetry(fixture.home); } + } + }); + + test("a claim without matching install-id, registration or Desktop executable refuses the launch", () => { + for (const fixture of [ + fakeDesktopHome({ installId: "other-install" }), + fakeDesktopHome({ installId: "install-a", plist: "absent" }), + fakeDesktopHome({ installId: "install-a", label: "OtherApp" }), + fakeDesktopHome({ installId: "install-a", args: [join(tmpdir(), "elsewhere", "opencodex-desktop")] }), + fakeDesktopHome({ installId: "install-a", args: ["executable-only-name"] }), + fakeDesktopHome({ installId: "install-a", identifier: "com.example.fake" }), + ]) { + try { + expect(launch(fixture)).toBe(false); + expect(fixture.seen.flat().includes("com.opencodex.desktop")).toBe(false); + expect(fixture.seen[0]?.[0]).not.toBe("/usr/bin/open"); + } finally { removeTreeWithRetry(fixture.home); } + } + // install-id store absent entirely. + const noId = fakeDesktopHome(); + try { expect(launch(noId)).toBe(false); } finally { removeTreeWithRetry(noId.home); } + }); + + test("a claim that is not a recorded Desktop owner, or a failing lookup, never launches", () => { + const fixture = fakeDesktopHome({ installId: "install-a" }); + try { + expect(launch(fixture, () => ({ kind: "none", revision: 2 }))).toBe(false); + expect(launch(fixture, () => ({ kind: "owned", ownership: { owner: "cli", installId: "install-a", consentGeneration: 1 }, revision: 2 }))).toBe(false); + expect(fixture.seen).toEqual([]); + } finally { removeTreeWithRetry(fixture.home); } + expect(launchDesktopApp({ platform: "darwin", home: mkdtempSync(join(tmpdir(), "ocx-desktop-empty-")), ownership: () => owned, run: () => { throw new Error("no app"); } })).toBe(false); expect(launchDesktopApp({ platform: "linux", run: () => { throw new Error("must not run"); } })).toBe(false); }); }); From 06e330d20432b0f3d03852223b0a5efa86185b57 Mon Sep 17 00:00:00 2001 From: GOAT Date: Thu, 15 Oct 2026 16:26:38 +0900 Subject: [PATCH 2/5] fix(startup): bind registered bundle launch to its main executable Reject relative launch registrations and non-file executable targets. Match CFBundleExecutable to the canonical registered Desktop binary before opening the recorded bundle. Exercise the actual macOS plist conversion without launching an app, and document manual recovery for inconsistent evidence. Co-authored-by: Epinephrine <27862058+luvs01@users.noreply.github.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/content/docs/guides/desktop-app.md | 8 +- src/cli/ensure-desktop-cold-start.ts | 13 +- structure/gui-and-management-api.md | 2 +- .../cli/cli-ensure-desktop-cold-start.test.ts | 113 +++++++++++++++--- 4 files changed, 112 insertions(+), 24 deletions(-) diff --git a/docs-site/src/content/docs/guides/desktop-app.md b/docs-site/src/content/docs/guides/desktop-app.md index 4ecad314138..a6c94f31389 100644 --- a/docs-site/src/content/docs/guides/desktop-app.md +++ b/docs-site/src/content/docs/guides/desktop-app.md @@ -116,8 +116,12 @@ means the registration could not be read. While the app runs the proxy, the Star background service installer disabled; to use it instead, quit OpenCodex and run `ocx service install` in a terminal. The Codex launcher shim can still be installed there. When a Codex launch finds no proxy, the shim waits for OpenCodex to restart it, opens -OpenCodex on macOS if it quit, and otherwise reports an error instead of starting a second -proxy. If the shim reads "activation not confirmed", add +OpenCodex on macOS if it quit and its recorded ownership matches a valid **Start at Login** +registration, and otherwise reports an error instead of starting a second proxy. Automatic +reopening selects that registration's app bundle path and checks that its declared main +executable matches the registered Desktop executable. If registration is missing, stale or +inconsistent, open OpenCodex manually; another copy with the same bundle identifier is not +selected as a fallback. If the shim reads "activation not confirmed", add `. ~/.opencodex/codex-shell-env.sh` to your shell startup file after other PATH setup. The bundled CLI is `/Applications/OpenCodex.app/Contents/MacOS/ocx` on macOS and diff --git a/src/cli/ensure-desktop-cold-start.ts b/src/cli/ensure-desktop-cold-start.ts index 43ceeffab2d..d6a89fd3f95 100644 --- a/src/cli/ensure-desktop-cold-start.ts +++ b/src/cli/ensure-desktop-cold-start.ts @@ -25,7 +25,7 @@ import { execFileSync } from "node:child_process"; import { randomUUID } from "node:crypto"; import { accessSync, constants, existsSync, linkSync, readdirSync, readFileSync, readlinkSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; -import { basename, dirname, join, win32 } from "node:path"; +import { basename, dirname, isAbsolute, join, win32 } from "node:path"; import { getConfigDir } from "../config/paths"; import { resolveServiceOwnership } from "../service/state"; @@ -246,18 +246,21 @@ function consentedDesktopBundle(home: string, installId: string, run: Execute): // The shapes auto-launch writes: [program] or [program, "--autostart"]; see the migration // rules in desktop/src-tauri/src/login_registration.rs, which classify anything else Leave. if (registration.Label !== DESKTOP_PLIST_LABEL || !Array.isArray(args) - || (args.length !== 1 && args.length !== 2) || typeof args[0] !== "string" + || (args.length !== 1 && args.length !== 2) || typeof args[0] !== "string" || !isAbsolute(args[0]) || (args.length === 2 && args[1] !== "--autostart") || (registration.Program !== undefined && registration.Program !== args[0])) return null; const executable = realpathSync(args[0]); - if (!isDesktopExecutable(executable, "darwin")) return null; + if (!isDesktopExecutable(executable, "darwin") || !statSync(executable).isFile()) return null; accessSync(executable, constants.X_OK); const bundle = dirname(dirname(dirname(executable))); if (!bundle.endsWith(".app")) return null; const info = JSON.parse( run("/usr/bin/plutil", ["-convert", "json", "-o", "-", join(bundle, "Contents", "Info.plist")], 3_000), - ) as { CFBundleIdentifier?: unknown }; - return info.CFBundleIdentifier === DESKTOP_BUNDLE_ID ? bundle : null; + ) as { CFBundleIdentifier?: unknown; CFBundleExecutable?: unknown }; + // `open` uses this selector, so checking only the registration and identifier would + // validate one binary while launching another. The registered path is already canonical. + if (info.CFBundleIdentifier !== DESKTOP_BUNDLE_ID || info.CFBundleExecutable !== "opencodex-desktop") return null; + return realpathSync(join(bundle, "Contents", "MacOS", info.CFBundleExecutable)) === executable ? bundle : null; } /** Start the consented macOS app hidden, by the path its own login registration recorded. */ diff --git a/structure/gui-and-management-api.md b/structure/gui-and-management-api.md index 61282cf2c13..239ff5de329 100644 --- a/structure/gui-and-management-api.md +++ b/structure/gui-and-management-api.md @@ -499,7 +499,7 @@ unvalidated Bun builds is unchanged (`src/lib/bun-stream-caps.ts`). ## Startup safety -Startup safety credits macOS and Linux `desktop` protection only after matching the durable app ownership or live supervision, an enabled login registration (macOS: loaded in this session or registered for the next login; Linux: enabled XDG autostart entry), and live app-to-bundled-proxy process relationship. This does not claim an independently installed CLI service. Missing or stale evidence remains at risk. The durable desktop claim remains visible when identity, login registration, or supervision fails; service install and repair controls and their copyable command stay disabled while the app owns or supervises the runtime. Shim controls stay enabled because `ocx ensure` (`src/cli/ensure-desktop-cold-start.ts`) waits for a running Desktop (matched by executable identity), relaunches a claimed macOS Desktop under a pid-held launch lease, and otherwise fails rather than starting a CLI proxy, including when ownership or the process list cannot be read. The relaunch runs the path the consented installation's own login registration recorded, after the app-local install id is checked against the recorded claim and the resolved bundle path ends in `.app`; a bundle-identifier lookup never chooses the launch target, so a claim whose registration or id evidence cannot be verified is refused rather than guessed. Under Desktop the shim installer child gets the login-shell PATH (`src/server/login-shell-path.ts`); an intact overlay not active on the dashboard's PATH reports `shimActivationPending` and shows the activation step instead of Repair. The Desktop row says "Running · Start at Login not verified" (`desktopBadgeKey` in `gui/src/startup-health-ui.ts`) for a fresh reading of a running owned or supervised app without verified login registration, and "Installed but unhealthy" otherwise; failed or stale readings never get the narrower label. Recovery guidance asks the user to check Start at Login and, when it is already checked, the system's login-item allowance. Compiled startup probes and the dashboard installer child (`runCliInstall` in `src/server/startup-action-control.ts`) use the standalone-aware self-launch argument builder. +Startup safety credits macOS and Linux `desktop` protection only after matching the durable app ownership or live supervision, an enabled login registration (macOS: loaded in this session or registered for the next login; Linux: enabled XDG autostart entry), and live app-to-bundled-proxy process relationship. This does not claim an independently installed CLI service. Missing or stale evidence remains at risk. The durable desktop claim remains visible when identity, login registration, or supervision fails; service install and repair controls and their copyable command stay disabled while the app owns or supervises the runtime. Shim controls stay enabled because `ocx ensure` (`src/cli/ensure-desktop-cold-start.ts`) waits for a running Desktop (matched by executable identity), relaunches a claimed macOS Desktop under a pid-held launch lease, and otherwise fails rather than starting a CLI proxy, including when ownership or the process list cannot be read. The relaunch selects the absolute path recorded by the installation's login registration, after the app-local install id matches the claim, the canonical executable is a regular executable file inside a `.app`, and that bundle's `CFBundleIdentifier` and `CFBundleExecutable` identify the same Desktop executable. A bundle-identifier lookup never chooses the launch target; missing, relative or inconsistent registration evidence refuses with manual-open guidance. Under Desktop the shim installer child gets the login-shell PATH (`src/server/login-shell-path.ts`); an intact overlay not active on the dashboard's PATH reports `shimActivationPending` and shows the activation step instead of Repair. The Desktop row says "Running · Start at Login not verified" (`desktopBadgeKey` in `gui/src/startup-health-ui.ts`) for a fresh reading of a running owned or supervised app without verified login registration, and "Installed but unhealthy" otherwise; failed or stale readings never get the narrower label. Recovery guidance asks the user to check Start at Login and, when it is already checked, the system's login-item allowance. Compiled startup probes and the dashboard installer child (`runCliInstall` in `src/server/startup-action-control.ts`) use the standalone-aware self-launch argument builder. **Startup safety** is reachable by route (`/#startup`) and rendered by the app, but it is not a sidebar entry: it is entered from the dashboard's startup-state row, which links there whether the diff --git a/tests/cli/cli-ensure-desktop-cold-start.test.ts b/tests/cli/cli-ensure-desktop-cold-start.test.ts index 94c397b3f2f..5b59a189d93 100644 --- a/tests/cli/cli-ensure-desktop-cold-start.test.ts +++ b/tests/cli/cli-ensure-desktop-cold-start.test.ts @@ -1,7 +1,8 @@ import { describe, expect, test } from "bun:test"; +import { execFileSync } from "node:child_process"; import { chmodSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { join, relative } from "node:path"; import { acquireDesktopLaunchLease, DESKTOP_COLD_START_TIMEOUT_MS, @@ -166,35 +167,36 @@ describe("Desktop process detection by executable identity", () => { }; /** A fake consented install: app-local install-id, a LaunchAgent plist, and a bundle on disk. */ function fakeDesktopHome(options: { - installId?: string; args?: string[]; bareArgs?: boolean; label?: string; identifier?: string; - plist?: "present" | "absent"; executable?: "present" | "absent"; bundleName?: string; + installId?: string; args?: unknown; bareArgs?: boolean; label?: string; identifier?: string; + plist?: "present" | "absent"; executable?: "present" | "absent" | "directory"; bundleName?: string; + bundleExecutable?: unknown; program?: unknown; relativeProgram?: boolean; } = {}) { const home = mkdtempSync(join(tmpdir(), "ocx-desktop-home-")); const bundle = join(home, "Applications", options.bundleName ?? "OpenCodex.app"); const executable = join(bundle, "Contents", "MacOS", "opencodex-desktop"); mkdirSync(join(bundle, "Contents", "MacOS"), { recursive: true }); - if (options.executable !== "absent") { writeFileSync(executable, ""); chmodSync(executable, 0o755); } - writeFileSync(join(bundle, "Contents", "Info.plist"), ""); + if (options.executable === "directory") mkdirSync(executable); + else if (options.executable !== "absent") { writeFileSync(executable, ""); chmodSync(executable, 0o755); } + const info = { CFBundleIdentifier: options.identifier ?? "com.opencodex.desktop", + CFBundleExecutable: "bundleExecutable" in options ? options.bundleExecutable : "opencodex-desktop" }; + writeFileSync(join(bundle, "Contents", "Info.plist"), JSON.stringify(info)); const store = join(home, "Library", "Application Support", "com.opencodex.desktop"); mkdirSync(store, { recursive: true }); if (options.installId !== undefined) writeFileSync(join(store, "install-id"), `${options.installId}\n`); mkdirSync(join(home, "Library", "LaunchAgents"), { recursive: true }); - if (options.plist !== "absent") writeFileSync(join(home, "Library", "LaunchAgents", "OpenCodex.plist"), ""); + const program = options.relativeProgram ? relative(process.cwd(), executable) : executable; + const registration = { Label: options.label ?? "OpenCodex", RunAtLoad: true, + ProgramArguments: options.bareArgs ? [program] : (options.args ?? [program, "--autostart"]), + ...("program" in options ? { Program: options.program } : {}) }; + if (options.plist !== "absent") writeFileSync(join(home, "Library", "LaunchAgents", "OpenCodex.plist"), JSON.stringify(registration)); const seen: string[][] = []; const run = (command: string, args: string[]) => { seen.push([command, ...args]); const target = args[args.length - 1] ?? ""; if (command !== "/usr/bin/plutil") return ""; - if (target.endsWith(`${"OpenCodex"}.plist`)) { - return JSON.stringify({ - Label: options.label ?? "OpenCodex", - ProgramArguments: options.bareArgs ? [executable] : (options.args ?? [executable, "--autostart"]), - }); - } - if (target.endsWith("Info.plist")) return JSON.stringify({ CFBundleIdentifier: options.identifier ?? "com.opencodex.desktop" }); - return ""; + return readFileSync(target, "utf8"); }; - return { home, bundle, seen, run }; + return { home, bundle, executable, seen, run }; } const launch = (fixture: ReturnType, ownership: () => ServiceOwnershipResolution = () => owned) => launchDesktopApp({ platform: "darwin", home: fixture.home, ownership, run: fixture.run }); @@ -234,6 +236,86 @@ describe("Desktop process detection by executable identity", () => { } }); + test("an existing relative registration path cannot select a bundle from the caller's working directory", () => { + const fixture = fakeDesktopHome({ installId: "install-a", relativeProgram: true }); + try { + expect(launch(fixture)).toBe(false); + expect(fixture.seen.some(call => call[0] === "/usr/bin/open")).toBe(false); + } finally { removeTreeWithRetry(fixture.home); } + }); + + test("bundle launch requires the declared main executable to match the registered regular file", () => { + for (const options of [{ bundleExecutable: "other" }, { bundleExecutable: undefined }, + { bundleExecutable: "../opencodex-desktop" }, { bundleExecutable: ["opencodex-desktop"] }, + { executable: "directory" as const }, { executable: "absent" as const }]) { + const fixture = fakeDesktopHome({ installId: "install-a", ...options }); + try { + expect(launch(fixture)).toBe(false); + expect(fixture.seen.some(call => call[0] === "/usr/bin/open")).toBe(false); + } finally { removeTreeWithRetry(fixture.home); } + } + }); + + test("malformed registration arguments and a conflicting Program never launch", () => { + for (const options of [{ args: [] }, { args: [123] }, { args: "opencodex-desktop" }, + { args: ["/some/app", "--other"] }, { args: ["/some/app", "--autostart", "extra"] }, + { program: "/another/app" }, { program: null }]) { + const fixture = fakeDesktopHome({ installId: "install-a", ...options }); + try { expect(launch(fixture)).toBe(false); expect(fixture.seen).toHaveLength(1); } + finally { removeTreeWithRetry(fixture.home); } + } + }); + + test("a matching optional Program is accepted, and conversion or open failures refuse", () => { + const fixture = fakeDesktopHome({ installId: "install-a" }); + try { + const plist = join(fixture.home, "Library", "LaunchAgents", "OpenCodex.plist"); + const registration = JSON.parse(readFileSync(plist, "utf8")); + writeFileSync(plist, JSON.stringify({ ...registration, Program: fixture.executable })); + expect(launch(fixture)).toBe(true); + for (const failingCommand of ["/usr/bin/plutil", "/usr/bin/open"]) { + expect(launchDesktopApp({ platform: "darwin", home: fixture.home, ownership: () => owned, + run: (command, args) => { if (command === failingCommand) throw new Error("fixture failure"); return fixture.run(command, args); }, + })).toBe(false); + } + writeFileSync(plist, "malformed plist"); + expect(launch(fixture)).toBe(false); + } finally { removeTreeWithRetry(fixture.home); } + }); + + test.skipIf(process.platform === "win32")("a non-executable registered file refuses launch", () => { + const fixture = fakeDesktopHome({ installId: "install-a" }); + try { + chmodSync(fixture.executable, 0o644); + expect(launch(fixture)).toBe(false); + expect(fixture.seen.some(call => call[0] === "/usr/bin/open")).toBe(false); + } finally { removeTreeWithRetry(fixture.home); } + }); + + test.skipIf(process.platform !== "darwin")("native plutil decodes the producer's XML registration and bundle selector", () => { + const fixture = fakeDesktopHome({ installId: "install-a" }); + try { + const plist = join(fixture.home, "Library", "LaunchAgents", "OpenCodex.plist"); + for (const path of [plist, join(fixture.bundle, "Contents", "Info.plist")]) { + execFileSync("/usr/bin/plutil", ["-convert", "xml1", path]); + } + const seen: string[][] = []; + const run = (command: string, args: string[]) => { + seen.push([command, ...args]); + if (command === "/usr/bin/open") return ""; + return execFileSync(command, args, { encoding: "utf8" }); + }; + expect(launchDesktopApp({ platform: "darwin", home: fixture.home, ownership: () => owned, run })).toBe(true); + expect(seen.at(-1)).toEqual(["/usr/bin/open", "-g", realpathSync(fixture.bundle), "--args", "--autostart"]); + writeFileSync(join(fixture.bundle, "Contents", "Info.plist"), JSON.stringify({ + CFBundleIdentifier: "com.opencodex.desktop", CFBundleExecutable: "other", + })); + seen.length = 0; + expect(launchDesktopApp({ platform: "darwin", home: fixture.home, ownership: () => owned, run })).toBe(false); + expect(seen.some(call => call[0] === "/usr/bin/open")).toBe(false); + } finally { removeTreeWithRetry(fixture.home); } + }); + test("a claim without matching install-id, registration or Desktop executable refuses the launch", () => { for (const fixture of [ fakeDesktopHome({ installId: "other-install" }), @@ -261,7 +343,6 @@ describe("Desktop process detection by executable identity", () => { expect(launch(fixture, () => ({ kind: "owned", ownership: { owner: "cli", installId: "install-a", consentGeneration: 1 }, revision: 2 }))).toBe(false); expect(fixture.seen).toEqual([]); } finally { removeTreeWithRetry(fixture.home); } - expect(launchDesktopApp({ platform: "darwin", home: mkdtempSync(join(tmpdir(), "ocx-desktop-empty-")), ownership: () => owned, run: () => { throw new Error("no app"); } })).toBe(false); expect(launchDesktopApp({ platform: "linux", run: () => { throw new Error("must not run"); } })).toBe(false); }); }); From 1500f2601babc09c4433fa5d1adc7009f9d2499f Mon Sep 17 00:00:00 2001 From: GOAT Date: Thu, 15 Oct 2026 16:35:58 +0900 Subject: [PATCH 3/5] docs(ko): match the registered-bundle reopen condition in the Desktop guide --- docs-site/src/content/docs/ko/guides/desktop-app.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs-site/src/content/docs/ko/guides/desktop-app.md b/docs-site/src/content/docs/ko/guides/desktop-app.md index 446a6e20c64..e3f175f9d5d 100644 --- a/docs-site/src/content/docs/ko/guides/desktop-app.md +++ b/docs-site/src/content/docs/ko/guides/desktop-app.md @@ -91,8 +91,12 @@ macOS에서 메뉴의 **Start at Login** 항목은 시작 안전 페이지도 **status unknown**은 등록을 읽지 못했다는 뜻입니다. 앱이 프록시를 실행하는 동안 시작 안전 페이지는 백그라운드 서비스 설치를 막아 둡니다. 서비스를 대신 쓰려면 OpenCodex를 종료하고 터미널에서 `ocx service install`을 실행하세요. Codex 런처 shim은 이 페이지에서 설치할 수 있습니다. -Codex를 실행할 때 프록시가 없으면 shim은 OpenCodex가 프록시를 다시 띄우기를 기다리고, macOS에서 -앱이 종료돼 있으면 앱을 열며, 그 밖의 경우에는 두 번째 프록시를 띄우지 않고 오류를 알립니다. +Codex를 실행할 때 프록시가 없으면 shim은 OpenCodex가 프록시를 다시 띄우기를 기다립니다. macOS에서 +앱이 종료돼 있고 기록된 소유 정보가 유효한 **로그인 시 시작** 등록과 일치하면 앱을 열고, 그 밖의 +경우에는 두 번째 프록시를 띄우지 않고 오류를 알립니다. 자동으로 다시 열 때는 그 등록에 기록된 앱 +번들 경로를 사용하고, 번들이 선언한 주 실행 파일이 등록된 Desktop 실행 파일과 같은지 확인합니다. +등록이 없거나 오래됐거나 서로 맞지 않으면 OpenCodex를 직접 여세요. 번들 식별자가 같은 다른 사본을 +대신 고르지 않습니다. shim이 "활성화 미확인"으로 보이면 셸 시작 파일의 다른 PATH 설정 뒤에 `. ~/.opencodex/codex-shell-env.sh`를 추가하세요. From 0c04904aa5d888e52ed338f2019b2725451cae52 Mon Sep 17 00:00:00 2001 From: GOAT Date: Thu, 15 Oct 2026 16:36:15 +0900 Subject: [PATCH 4/5] docs(ko): keep the Start at Login menu label --- docs-site/src/content/docs/ko/guides/desktop-app.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-site/src/content/docs/ko/guides/desktop-app.md b/docs-site/src/content/docs/ko/guides/desktop-app.md index e3f175f9d5d..37c38585ff1 100644 --- a/docs-site/src/content/docs/ko/guides/desktop-app.md +++ b/docs-site/src/content/docs/ko/guides/desktop-app.md @@ -92,7 +92,7 @@ macOS에서 메뉴의 **Start at Login** 항목은 시작 안전 페이지도 백그라운드 서비스 설치를 막아 둡니다. 서비스를 대신 쓰려면 OpenCodex를 종료하고 터미널에서 `ocx service install`을 실행하세요. Codex 런처 shim은 이 페이지에서 설치할 수 있습니다. Codex를 실행할 때 프록시가 없으면 shim은 OpenCodex가 프록시를 다시 띄우기를 기다립니다. macOS에서 -앱이 종료돼 있고 기록된 소유 정보가 유효한 **로그인 시 시작** 등록과 일치하면 앱을 열고, 그 밖의 +앱이 종료돼 있고 기록된 소유 정보가 유효한 **Start at Login** 등록과 일치하면 앱을 열고, 그 밖의 경우에는 두 번째 프록시를 띄우지 않고 오류를 알립니다. 자동으로 다시 열 때는 그 등록에 기록된 앱 번들 경로를 사용하고, 번들이 선언한 주 실행 파일이 등록된 Desktop 실행 파일과 같은지 확인합니다. 등록이 없거나 오래됐거나 서로 맞지 않으면 OpenCodex를 직접 여세요. 번들 식별자가 같은 다른 사본을 From 6c14932bcb3574e2cb8b7750aa7fbdba74abff74 Mon Sep 17 00:00:00 2001 From: GOAT Date: Thu, 15 Oct 2026 20:07:41 +0900 Subject: [PATCH 5/5] fix(startup): validate LaunchAgent paths with POSIX semantics Keep the macOS registration contract independent of the test host. Place fixtures on the caller's drive so Windows relative() cannot return an absolute cross-drive target, and assert the rejected path is truly relative. Exercise Windows drive paths and non-macOS command refusal explicitly. Co-authored-by: Epinephrine <27862058+luvs01@users.noreply.github.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- src/cli/ensure-desktop-cold-start.ts | 5 ++-- structure/runtime.md | 2 +- .../cli/cli-ensure-desktop-cold-start.test.ts | 25 ++++++++++++++----- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/cli/ensure-desktop-cold-start.ts b/src/cli/ensure-desktop-cold-start.ts index d6a89fd3f95..60b5f371310 100644 --- a/src/cli/ensure-desktop-cold-start.ts +++ b/src/cli/ensure-desktop-cold-start.ts @@ -25,7 +25,7 @@ import { execFileSync } from "node:child_process"; import { randomUUID } from "node:crypto"; import { accessSync, constants, existsSync, linkSync, readdirSync, readFileSync, readlinkSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; -import { basename, dirname, isAbsolute, join, win32 } from "node:path"; +import { basename, dirname, join, posix, win32 } from "node:path"; import { getConfigDir } from "../config/paths"; import { resolveServiceOwnership } from "../service/state"; @@ -245,8 +245,9 @@ function consentedDesktopBundle(home: string, installId: string, run: Execute): const args = registration.ProgramArguments; // The shapes auto-launch writes: [program] or [program, "--autostart"]; see the migration // rules in desktop/src-tauri/src/login_registration.rs, which classify anything else Leave. + // This is a macOS plist path, regardless of the host running an injected launch probe. if (registration.Label !== DESKTOP_PLIST_LABEL || !Array.isArray(args) - || (args.length !== 1 && args.length !== 2) || typeof args[0] !== "string" || !isAbsolute(args[0]) + || (args.length !== 1 && args.length !== 2) || typeof args[0] !== "string" || !posix.isAbsolute(args[0]) || (args.length === 2 && args[1] !== "--autostart") || (registration.Program !== undefined && registration.Program !== args[0])) return null; const executable = realpathSync(args[0]); diff --git a/structure/runtime.md b/structure/runtime.md index 2a31215cb52..0a1d5399623 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -543,7 +543,7 @@ Codex compaction uses a request-local model override for the configured triggers state, beside the install provenance. The claim carries an `owner` (`cli` or `desktop`), an opaque `installId` naming the owning installation rather than the user or the machine, and a `consentGeneration`. An absent claim means the CLI install that registered the service owns -the runtime, which is what every record written before the field existed says. `src/service/desktop-startup.ts` separates the durable macOS, Linux and Windows desktop claim from startup viability. Windows Desktop identity is read from `%APPDATA%\com.opencodex.desktop\install-id` and checked against live parentage; Windows Start at Login is unverified and Desktop never receives startup protection credit there. Independent [live Desktop supervision](desktop-shell.md#runtime-ownership-from-the-apps-side) can also grant startup protection without a claim when that app's login registration is verified; it never becomes ownership. On Linux, auto-launch 0.5.0 writes the login entry under HOME/.config/autostart, so a relocated XDG_CONFIG_HOME (outside that search path) is uncredited. Conditional entries and ambiguous Exec syntax are uncredited; the resolved desktop basename must match and its resolved ocx stays in the same directory. Both complete evidence reads must agree on ownership, install-id, entry contents, executable targets, PID and parent relationship; changes fail closed. AppImage registrations are uncredited because they name the outer image rather than the mounted executable. `src/server/startup-health-cache.ts` launches source and compiled probes with `selfLaunchArgv`, revokes stale protection, and preserves desktop recovery guidance. An enabled, identity-matching macOS LaunchAgent registration remains viable when `launchctl print` specifically proves the job is absent in the current session. `src/service/desktop-startup.ts` reports `registered-not-loaded` for next login; wrong plist paths/arguments/label, disabled override, unknown failures and supervisor races still refuse credit. `src/codex/autostart-health.ts` distinguishes this protected registration in restart safety. `src/service/linux-linger.ts` uses bounded `loginctl show-user` to report already-enabled linger, advise activation only when disabled, or explicitly report unavailable diagnosis. +the runtime, which is what every record written before the field existed says. `src/service/desktop-startup.ts` separates the durable macOS, Linux and Windows desktop claim from startup viability. Windows Desktop identity is read from `%APPDATA%\com.opencodex.desktop\install-id` and checked against live parentage; Windows Start at Login is unverified and Desktop never receives startup protection credit there. Independent [live Desktop supervision](desktop-shell.md#runtime-ownership-from-the-apps-side) can also grant startup protection without a claim when that app's login registration is verified; it never becomes ownership. On Linux, auto-launch 0.5.0 writes the login entry under HOME/.config/autostart, so a relocated XDG_CONFIG_HOME (outside that search path) is uncredited. Conditional entries and ambiguous Exec syntax are uncredited; the resolved desktop basename must match and its resolved ocx stays in the same directory. Both complete evidence reads must agree on ownership, install-id, entry contents, executable targets, PID and parent relationship; changes fail closed. AppImage registrations are uncredited because they name the outer image rather than the mounted executable. `src/server/startup-health-cache.ts` launches source and compiled probes with `selfLaunchArgv`, revokes stale protection, and preserves desktop recovery guidance. An enabled, identity-matching macOS LaunchAgent registration remains viable when `launchctl print` specifically proves the job is absent in the current session. `src/service/desktop-startup.ts` reports `registered-not-loaded` for next login; wrong plist paths/arguments/label, disabled override, unknown failures and supervisor races still refuse credit. `src/codex/autostart-health.ts` distinguishes this protected registration in restart safety. `src/service/linux-linger.ts` uses bounded `loginctl show-user` to report already-enabled linger, advise activation only when disabled, or explicitly report unavailable diagnosis. `src/cli/ensure-desktop-cold-start.ts` launches a claimed Desktop only on macOS. Its LaunchAgent executable path must be POSIX-absolute, including when the launch probe is exercised on another host; Windows drive paths do not satisfy that plist contract. Launches on Linux and Windows refuse without invoking a command. Every write goes through `swapServiceInstallState`. With a custom home, the default-home record is the authority every writer can derive and the active-home record is a compatibility diff --git a/tests/cli/cli-ensure-desktop-cold-start.test.ts b/tests/cli/cli-ensure-desktop-cold-start.test.ts index 5b59a189d93..5b1ec4ce80e 100644 --- a/tests/cli/cli-ensure-desktop-cold-start.test.ts +++ b/tests/cli/cli-ensure-desktop-cold-start.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"; import { execFileSync } from "node:child_process"; import { chmodSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { join, relative } from "node:path"; +import { isAbsolute, join, relative } from "node:path"; import { acquireDesktopLaunchLease, DESKTOP_COLD_START_TIMEOUT_MS, @@ -171,7 +171,10 @@ describe("Desktop process detection by executable identity", () => { plist?: "present" | "absent"; executable?: "present" | "absent" | "directory"; bundleName?: string; bundleExecutable?: unknown; program?: unknown; relativeProgram?: boolean; } = {}) { - const home = mkdtempSync(join(tmpdir(), "ocx-desktop-home-")); + // Keep the bundle on cwd's drive: Windows relative() returns an absolute path across drives. + const fixtureRoot = join(process.cwd(), ".tmp"); + mkdirSync(fixtureRoot, { recursive: true }); + const home = mkdtempSync(join(fixtureRoot, "ocx-desktop-home-")); const bundle = join(home, "Applications", options.bundleName ?? "OpenCodex.app"); const executable = join(bundle, "Contents", "MacOS", "opencodex-desktop"); mkdirSync(join(bundle, "Contents", "MacOS"), { recursive: true }); @@ -184,7 +187,9 @@ describe("Desktop process detection by executable identity", () => { mkdirSync(store, { recursive: true }); if (options.installId !== undefined) writeFileSync(join(store, "install-id"), `${options.installId}\n`); mkdirSync(join(home, "Library", "LaunchAgents"), { recursive: true }); - const program = options.relativeProgram ? relative(process.cwd(), executable) : executable; + // A root-relative POSIX spelling also reaches this same-drive fixture on Windows. + const absoluteProgram = process.platform === "win32" ? executable.slice(2).replaceAll("\\", "/") : executable; + const program = options.relativeProgram ? relative(process.cwd(), executable) : absoluteProgram; const registration = { Label: options.label ?? "OpenCodex", RunAtLoad: true, ProgramArguments: options.bareArgs ? [program] : (options.args ?? [program, "--autostart"]), ...("program" in options ? { Program: options.program } : {}) }; @@ -196,7 +201,7 @@ describe("Desktop process detection by executable identity", () => { if (command !== "/usr/bin/plutil") return ""; return readFileSync(target, "utf8"); }; - return { home, bundle, executable, seen, run }; + return { home, bundle, executable, program, seen, run }; } const launch = (fixture: ReturnType, ownership: () => ServiceOwnershipResolution = () => owned) => launchDesktopApp({ platform: "darwin", home: fixture.home, ownership, run: fixture.run }); @@ -239,6 +244,8 @@ describe("Desktop process detection by executable identity", () => { test("an existing relative registration path cannot select a bundle from the caller's working directory", () => { const fixture = fakeDesktopHome({ installId: "install-a", relativeProgram: true }); try { + expect(isAbsolute(fixture.program)).toBe(false); + expect(realpathSync(fixture.program)).toBe(realpathSync(fixture.executable)); expect(launch(fixture)).toBe(false); expect(fixture.seen.some(call => call[0] === "/usr/bin/open")).toBe(false); } finally { removeTreeWithRetry(fixture.home); } @@ -259,6 +266,8 @@ describe("Desktop process detection by executable identity", () => { test("malformed registration arguments and a conflicting Program never launch", () => { for (const options of [{ args: [] }, { args: [123] }, { args: "opencodex-desktop" }, { args: ["/some/app", "--other"] }, { args: ["/some/app", "--autostart", "extra"] }, + { args: ["C:\\OpenCodex.app\\Contents\\MacOS\\opencodex-desktop"] }, + { args: ["C:OpenCodex.app\\Contents\\MacOS\\opencodex-desktop"] }, { program: "/another/app" }, { program: null }]) { const fixture = fakeDesktopHome({ installId: "install-a", ...options }); try { expect(launch(fixture)).toBe(false); expect(fixture.seen).toHaveLength(1); } @@ -271,7 +280,7 @@ describe("Desktop process detection by executable identity", () => { try { const plist = join(fixture.home, "Library", "LaunchAgents", "OpenCodex.plist"); const registration = JSON.parse(readFileSync(plist, "utf8")); - writeFileSync(plist, JSON.stringify({ ...registration, Program: fixture.executable })); + writeFileSync(plist, JSON.stringify({ ...registration, Program: fixture.program })); expect(launch(fixture)).toBe(true); for (const failingCommand of ["/usr/bin/plutil", "/usr/bin/open"]) { expect(launchDesktopApp({ platform: "darwin", home: fixture.home, ownership: () => owned, @@ -343,7 +352,11 @@ describe("Desktop process detection by executable identity", () => { expect(launch(fixture, () => ({ kind: "owned", ownership: { owner: "cli", installId: "install-a", consentGeneration: 1 }, revision: 2 }))).toBe(false); expect(fixture.seen).toEqual([]); } finally { removeTreeWithRetry(fixture.home); } - expect(launchDesktopApp({ platform: "linux", run: () => { throw new Error("must not run"); } })).toBe(false); + for (const platform of ["linux", "win32"] as const) { + const commands: string[] = []; + expect(launchDesktopApp({ platform, run: command => { commands.push(command); return ""; } })).toBe(false); + expect(commands).toEqual([]); + } }); });