Skip to content
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
8 changes: 6 additions & 2 deletions docs-site/src/content/docs/guides/desktop-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
`. ~/.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
Expand Down
8 changes: 6 additions & 2 deletions docs-site/src/content/docs/ko/guides/desktop-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,12 @@ macOS에서 메뉴의 **Start at Login** 항목은 시작 안전 페이지도
**status unknown**은 등록을 읽지 못했다는 뜻입니다. 앱이 프록시를 실행하는 동안 시작 안전 페이지는
백그라운드 서비스 설치를 막아 둡니다. 서비스를 대신 쓰려면 OpenCodex를 종료하고
터미널에서 `ocx service install`을 실행하세요. Codex 런처 shim은 이 페이지에서 설치할 수 있습니다.
Codex를 실행할 때 프록시가 없으면 shim은 OpenCodex가 프록시를 다시 띄우기를 기다리고, macOS에서
앱이 종료돼 있으면 앱을 열며, 그 밖의 경우에는 두 번째 프록시를 띄우지 않고 오류를 알립니다.
Codex를 실행할 때 프록시가 없으면 shim은 OpenCodex가 프록시를 다시 띄우기를 기다립니다. macOS에서
앱이 종료돼 있고 기록된 소유 정보가 유효한 **Start at Login** 등록과 일치하면 앱을 열고, 그 밖의
경우에는 두 번째 프록시를 띄우지 않고 오류를 알립니다. 자동으로 다시 열 때는 그 등록에 기록된 앱
번들 경로를 사용하고, 번들이 선언한 주 실행 파일이 등록된 Desktop 실행 파일과 같은지 확인합니다.
등록이 없거나 오래됐거나 서로 맞지 않으면 OpenCodex를 직접 여세요. 번들 식별자가 같은 다른 사본을
대신 고르지 않습니다.
shim이 "활성화 미확인"으로 보이면 셸 시작 파일의 다른 PATH 설정 뒤에
`. ~/.opencodex/codex-shell-env.sh`를 추가하세요.

Expand Down
67 changes: 60 additions & 7 deletions src/cli/ensure-desktop-cold-start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -22,9 +23,9 @@
*/
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 { basename, dirname, join, posix, win32 } from "node:path";
import { getConfigDir } from "../config/paths";
import { resolveServiceOwnership } from "../service/state";

Expand Down Expand Up @@ -220,11 +221,63 @@ 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.
// 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" || !posix.isAbsolute(args[0])
|| (args.length === 2 && args[1] !== "--autostart")
|| (registration.Program !== undefined && registration.Program !== args[0])) return null;
Comment on lines +249 to +252

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require RunAtLoad for automatic reopening

When ~/Library/LaunchAgents/OpenCodex.plist is stale or edited so RunAtLoad is false or absent, this condition still accepts it because it checks only the label, arguments, and optional Program. launchDesktopApp then opens the application whenever a matching ownership claim exists, even though this is not the valid Start-at-Login registration described by the updated guide and the existing macLoginRegistration validator rejects the same plist. Require RunAtLoad === true before returning the bundle and cover the false/absent cases.

AGENTS.md reference: docs-site/AGENTS.md:L7-L10

Useful? React with 👍 / 👎.

const executable = realpathSync(args[0]);
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; 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. */
export function launchDesktopApp(options: {
platform?: NodeJS.Platform;
run?: Execute;
home?: string;
ownership?: () => ReturnType<typeof resolveServiceOwnership>;
} = {}): 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; }
}
Expand Down
2 changes: 1 addition & 1 deletion structure/gui-and-management-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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
Expand Down
2 changes: 1 addition & 1 deletion structure/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading