diff --git a/src/cli/ensure-desktop-cold-start.ts b/src/cli/ensure-desktop-cold-start.ts index 60b5f37131..a1784fae63 100644 --- a/src/cli/ensure-desktop-cold-start.ts +++ b/src/cli/ensure-desktop-cold-start.ts @@ -241,12 +241,12 @@ function consentedDesktopBundle(home: string, installId: string, run: Execute): 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 }; + ) as { Label?: unknown; RunAtLoad?: 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. // This is a macOS plist path, regardless of the host running an injected launch probe. - if (registration.Label !== DESKTOP_PLIST_LABEL || !Array.isArray(args) + if (registration.Label !== DESKTOP_PLIST_LABEL || registration.RunAtLoad !== true || !Array.isArray(args) || (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; diff --git a/structure/runtime.md b/structure/runtime.md index fd71a66ad1..979af5d298 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -534,7 +534,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. `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. +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 requires `RunAtLoad: true` and a POSIX-absolute executable path, including when the launch probe is exercised on another host; Windows drive paths do not satisfy that plist contract. Both `[program, "--autostart"]` and the legacy `[program]` arguments remain accepted. When this caller attempts the relaunch, a false or missing `RunAtLoad` returns manual-open guidance without waiting for readiness or falling back to a CLI proxy; an already-running Desktop still follows the wait path. Launches on Linux and Windows refuse without invoking a command. Every write goes through `swapServiceInstallState`. Guarded tests use only their selected home; see [test state isolation](ops/test-sandbox-cleanup.md). Production retains both paths. 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 5b1ec4ce80..5b9f58fbf1 100644 --- a/tests/cli/cli-ensure-desktop-cold-start.test.ts +++ b/tests/cli/cli-ensure-desktop-cold-start.test.ts @@ -169,7 +169,7 @@ describe("Desktop process detection by executable identity", () => { function fakeDesktopHome(options: { 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; + bundleExecutable?: unknown; program?: unknown; relativeProgram?: boolean; runAtLoad?: unknown; } = {}) { // Keep the bundle on cwd's drive: Windows relative() returns an absolute path across drives. const fixtureRoot = join(process.cwd(), ".tmp"); @@ -190,7 +190,7 @@ describe("Desktop process detection by executable identity", () => { // 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, + const registration = { Label: options.label ?? "OpenCodex", RunAtLoad: "runAtLoad" in options ? options.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)); @@ -223,6 +223,26 @@ describe("Desktop process detection by executable identity", () => { } finally { removeTreeWithRetry(fixture.home); } }); + test("a false or missing RunAtLoad refuses launch without waiting or falling back to a CLI proxy", async () => { + for (const runAtLoad of [false, undefined]) { + const fixture = fakeDesktopHome({ installId: "install-a", runAtLoad }); + try { + expect(launch(fixture)).toBe(false); + const calls = fresh(); + const dependencies = deps({ ownership: "desktop", process: "absent" }, calls); + dependencies.launchDesktop = () => launch(fixture); + const result = await runDesktopColdStart(dependencies); + let cliFallbacks = 0; + if (result.kind === "cli") cliFallbacks++; + expect(result.kind === "failed" && result.message).toContain("Open OpenCodex manually"); + expect(calls.waits).toEqual([]); + expect(calls.released).toBe(1); + expect(cliFallbacks).toBe(0); + expect(fixture.seen.filter(call => call[0] === "/usr/bin/open")).toHaveLength(0); + } 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 });