From 6580b774a0b5674fe620fbb6c7ea54cf35c8cb22 Mon Sep 17 00:00:00 2001 From: Chris Busillo Date: Thu, 23 Jul 2026 15:31:12 -0400 Subject: [PATCH] fix(package): select the local daemon transport --- scripts/codex_lab_package/README.md | 12 +- scripts/codex_lab_package/layout.py | 6 +- scripts/codex_lab_package/live_smoke.py | 125 ++++++++++++++++--- scripts/codex_lab_package/smoke.py | 12 +- scripts/codex_lab_package/test_layout.py | 25 ++-- scripts/codex_lab_package/test_live_smoke.py | 49 ++++++++ 6 files changed, 190 insertions(+), 39 deletions(-) diff --git a/scripts/codex_lab_package/README.md b/scripts/codex_lab_package/README.md index 46e9734ee454..95890a50f613 100644 --- a/scripts/codex_lab_package/README.md +++ b/scripts/codex_lab_package/README.md @@ -2,11 +2,13 @@ This helper builds a macOS `Codex Lab.app` launcher bundle. The bundle does not contain or modify OpenAI's signed desktop app. Instead, it embeds a Codex Lab -CLI binary, binds its source commit, version, and SHA-256 digest, sets -`CODEX_CLI_PATH` to that exact path, and launches through LaunchServices. The -official app is bound to the persistent engine by setting `CODEX_HOME` and -`CODEX_LAB_HOME` to the same Lab home and enabling -`CODEX_APP_SERVER_USE_LOCAL_DAEMON=1`. +CLI binary and binds its source commit, version, and SHA-256 digest. The official +app is bound to the persistent engine by setting `CODEX_HOME` and +`CODEX_LAB_HOME` to the same Lab home, enabling +`CODEX_APP_SERVER_USE_LOCAL_DAEMON=1`, and explicitly setting +`CODEX_CLI_PATH` and `CODEX_APP_SERVER_FORCE_CLI` to empty values. Current +official clients use a non-empty CLI path or `CODEX_APP_SERVER_FORCE_CLI=1` to +select stdio instead of the local daemon. The launcher accepts only intact `com.openai.codex` bundles signed by OpenAI team `2DC432GLL2`. After an optional build-time override, it checks system and diff --git a/scripts/codex_lab_package/layout.py b/scripts/codex_lab_package/layout.py index 93edf263c2d7..d9f06c57fb53 100644 --- a/scripts/codex_lab_package/layout.py +++ b/scripts/codex_lab_package/layout.py @@ -399,7 +399,7 @@ def _launcher_script( fi expected_daemon_socket="$LAB_HOME/app-server-control/app-server-control.sock" if [ "$daemon_backend" != "pid" ] \ - || [ "$daemon_managed_codex_path" != "$MANAGED_CLI" ] \ + || [ ! "$daemon_managed_codex_path" -ef "$MANAGED_CLI" ] \ || [ "$daemon_socket_path" != "$expected_daemon_socket" ] \ || [ "$daemon_app_server_version" != "$version" ]; then echo "Persistent Codex Lab daemon does not match the managed engine under $LAB_HOME." >&2 @@ -410,8 +410,10 @@ def _launcher_script( echo "Selected OpenAI coding desktop app: $CODEX_APP" >&2 echo "Codex Lab CLI provenance: commit=$source_commit dirty=$dirty_state profile=$build_profile channel=$build_channel version=$version" >&2 +unset CODEX_CLI_PATH CODEX_APP_SERVER_FORCE_CLI exec "$OPEN" -n \ - --env "CODEX_CLI_PATH=$LAB_CLI" \ + --env "CODEX_CLI_PATH=" \ + --env "CODEX_APP_SERVER_FORCE_CLI=" \ --env "CODEX_HOME=$LAB_HOME" \ --env "CODEX_LAB_HOME=$LAB_HOME" \ --env "CODEX_APP_SERVER_USE_LOCAL_DAEMON=1" \ diff --git a/scripts/codex_lab_package/live_smoke.py b/scripts/codex_lab_package/live_smoke.py index 79e84335f7ef..b73745b6116d 100644 --- a/scripts/codex_lab_package/live_smoke.py +++ b/scripts/codex_lab_package/live_smoke.py @@ -5,6 +5,7 @@ import ctypes import ctypes.util import json +import math import os from pathlib import Path import re @@ -25,6 +26,9 @@ SELECTED_APP_PREFIX = "Selected OpenAI coding desktop app: " SOURCE_COMMIT_PATTERN = re.compile(r"^(?:[0-9a-f]{40}|[0-9a-f]{64})$") MANAGED_CLI_RELATIVE_PATH = Path("packages/standalone/current/codex") +STABILITY_WINDOW_SECONDS = 5.0 +MIN_TIMEOUT_SECONDS = 10.0 +MAX_DESKTOP_LOG_BYTES = 512 * 1024 def main() -> None: @@ -44,8 +48,7 @@ def main() -> None: def run_live_smoke(app_dir: Path, timeout_seconds: float) -> dict[str, Any]: if sys.platform != "darwin": raise RuntimeError("live Codex Lab desktop smoke requires macOS") - if timeout_seconds <= 0: - raise ValueError("timeout seconds must be greater than zero") + validate_timeout_seconds(timeout_seconds) launcher = app_dir / "Contents/MacOS/Codex Lab Launcher" cli_path = app_dir / "Contents/Resources/codex-lab" @@ -66,7 +69,16 @@ def run_live_smoke(app_dir: Path, timeout_seconds: float) -> dict[str, Any]: validate_matching_build_provenance(provenance, managed_provenance) before_rows = read_process_rows() before_pids = {pid for pid, _ppid, _command in before_rows} - launch = subprocess.run([str(launcher)], capture_output=True, text=True, timeout=20) + deadline = time.monotonic() + timeout_seconds + try: + launch = subprocess.run( + [str(launcher)], + capture_output=True, + text=True, + timeout=max(0.1, deadline - time.monotonic()), + ) + except subprocess.TimeoutExpired as exc: + raise TimeoutError("timed out launching Codex Lab") from exc if launch.returncode != 0: raise RuntimeError( "Codex Lab launcher failed: " @@ -77,11 +89,12 @@ def run_live_smoke(app_dir: Path, timeout_seconds: float) -> dict[str, Any]: gui_executable = official_app_executable_path(selected_app) expected_environment = { "CODEX_APP_SERVER_USE_LOCAL_DAEMON": "1", - "CODEX_CLI_PATH": str(cli_path.resolve()), + "CODEX_APP_SERVER_FORCE_CLI": "", + "CODEX_CLI_PATH": "", "CODEX_HOME": str(lab_home), "CODEX_LAB_HOME": str(lab_home), } - deadline = time.monotonic() + timeout_seconds + stable_since = None while time.monotonic() < deadline: rows = read_process_rows() gui_pids = { @@ -98,7 +111,7 @@ def run_live_smoke(app_dir: Path, timeout_seconds: float) -> dict[str, Any]: pid for pid, _ppid, command in rows if pid not in before_pids - and "app-server" in command.split() + and is_serving_app_server_command(command) and process_has_ancestor(pid, gui_pids, rows) ] if new_gui_app_servers: @@ -106,17 +119,24 @@ def run_live_smoke(app_dir: Path, timeout_seconds: float) -> dict[str, Any]: "official app launched a bundled stdio app-server instead of the persistent daemon" ) managed_servers = sorted(matching_app_server_pids(rows, managed_cli_path)) - if matching_gui_pids and managed_servers: - return { - "appServerExecutablePath": str(managed_cli_path.resolve()), - "appServerPid": managed_servers[0], - "guiPids": matching_gui_pids, - "managedProvenance": managed_provenance, - "mode": "persistentLocalDaemon", - "officialAppPath": str(selected_app), - "provenance": provenance, - "schemaVersion": 1, - } + transport_proof = desktop_transport_proof(matching_gui_pids) + if matching_gui_pids and managed_servers and transport_proof is not None: + stable_since = stable_since or time.monotonic() + if time.monotonic() - stable_since >= STABILITY_WINDOW_SECONDS: + return { + "appServerExecutablePath": str(managed_cli_path.resolve()), + "appServerPid": managed_servers[0], + **transport_proof, + "guiPids": matching_gui_pids, + "managedProvenance": managed_provenance, + "mode": "persistentLocalDaemon", + "officialAppPath": str(selected_app), + "provenance": provenance, + "schemaVersion": 1, + "stabilityWindowSeconds": STABILITY_WINDOW_SECONDS, + } + else: + stable_since = None time.sleep(0.2) raise TimeoutError( f"timed out waiting for the official app to use {managed_cli_path}" @@ -141,6 +161,57 @@ def read_cli_provenance(cli_path: Path) -> dict[str, Any]: return {field: provenance[field] for field in PROVENANCE_FIELDS} +def validate_timeout_seconds(timeout_seconds: float) -> None: + if not math.isfinite(timeout_seconds) or timeout_seconds < MIN_TIMEOUT_SECONDS: + raise ValueError( + f"timeout seconds must be finite and at least {MIN_TIMEOUT_SECONDS}" + ) + + +def desktop_transport_proof( + gui_pids: list[int], *, log_root: Path | None = None +) -> dict[str, str] | None: + root = log_root or Path.home() / "Library/Logs/com.openai.codex" + for pid in gui_pids: + paths = sorted( + root.rglob(f"*-{pid}-t0-*.log"), + key=lambda path: path.stat().st_mtime, + reverse=True, + ) + for path in paths: + log = _bounded_file_tail(path, MAX_DESKTOP_LOG_BYTES) + if "stdio_transport_spawned" in log: + raise RuntimeError( + "official app log reported a bundled stdio app-server" + ) + lines = log.splitlines() + transport_ready = any( + "Transport start success" in line + and "hostId=local" in line + and "transport=websocket" in line + for line in lines + ) + initialized = any( + "initialize_handshake_result" in line + and "outcome=success" in line + and "transportKind=websocket" in line + for line in lines + ) + if transport_ready and initialized: + return { + "desktopLogPath": str(path.resolve()), + "desktopTransport": "websocket", + } + return None + + +def _bounded_file_tail(path: Path, limit: int) -> str: + with path.open("rb") as handle: + handle.seek(0, os.SEEK_END) + handle.seek(max(0, handle.tell() - limit)) + return handle.read(limit).decode(errors="replace") + + def validate_cli_provenance(provenance: Any, cli_path: Path) -> None: if not isinstance(provenance, dict) or provenance.get("schema_version") != 1: raise ValueError("Codex Lab CLI provenance schema is unsupported") @@ -207,10 +278,19 @@ def matching_app_server_pids( return [ pid for pid, _ppid, command in rows - if "app-server" in command.split() and executable_path(pid) == expected + if is_serving_app_server_command(command) and executable_path(pid) == expected ] +def is_serving_app_server_command(command: str) -> bool: + tokens = command.split() + return any( + token == "app-server" + and (index + 1 == len(tokens) or tokens[index + 1] != "daemon") + for index, token in enumerate(tokens) + ) + + def process_executable_path(pid: int) -> Path | None: try: libproc = ctypes.CDLL( @@ -230,16 +310,23 @@ def process_has_environment( pid: int, expected: dict[str, str], reader: Callable[[int], str] | None = None, + *, + forbidden: set[str] | None = None, ) -> bool: environment_reader = reader or read_process_environment try: process = environment_reader(pid) except (OSError, subprocess.SubprocessError): return False - return all( + has_expected = all( re.search(rf"(?:^|\s){re.escape(name)}={re.escape(value)}(?:\s|$)", process) for name, value in expected.items() ) + forbidden = forbidden or set() + has_forbidden = any( + re.search(rf"(?:^|\s){re.escape(name)}=", process) for name in forbidden + ) + return has_expected and not has_forbidden def read_process_environment(pid: int) -> str: diff --git a/scripts/codex_lab_package/smoke.py b/scripts/codex_lab_package/smoke.py index 2e20236987e6..dc02e70777c0 100644 --- a/scripts/codex_lab_package/smoke.py +++ b/scripts/codex_lab_package/smoke.py @@ -70,7 +70,12 @@ def smoke_check( ) launcher = launcher_path.read_text(encoding="utf-8") - _require_contains(launcher, "CODEX_CLI_PATH", launcher_path) + _require_contains( + launcher, "unset CODEX_CLI_PATH CODEX_APP_SERVER_FORCE_CLI", launcher_path + ) + _require_contains(launcher, '--env "CODEX_CLI_PATH="', launcher_path) + _require_contains(launcher, '--env "CODEX_APP_SERVER_FORCE_CLI="', launcher_path) + _require_not_contains(launcher, '--env "CODEX_CLI_PATH=$LAB_CLI"', launcher_path) _require_contains(launcher, "CODEX_HOME=$LAB_HOME", launcher_path) _require_contains(launcher, "CODEX_LAB_HOME=$LAB_HOME", launcher_path) _require_contains(launcher, "CODEX_APP_SERVER_USE_LOCAL_DAEMON=1", launcher_path) @@ -156,5 +161,10 @@ def _require_contains(contents: str, needle: str, path: Path) -> None: raise ValueError(f"{path}: missing {needle!r}") +def _require_not_contains(contents: str, needle: str, path: Path) -> None: + if needle in contents: + raise ValueError(f"{path}: unexpected {needle!r}") + + if __name__ == "__main__": raise SystemExit(main()) diff --git a/scripts/codex_lab_package/test_layout.py b/scripts/codex_lab_package/test_layout.py index 474c30312aa2..40d50d920a60 100644 --- a/scripts/codex_lab_package/test_layout.py +++ b/scripts/codex_lab_package/test_layout.py @@ -292,7 +292,6 @@ def test_launcher_executes_exact_cli_and_fails_closed_for_running_app(self) -> N encoding="utf-8", ) open_log = root / "open.log" - child_log = root / "child.log" fake_open = root / "open" fake_open.write_text( """#!/bin/sh @@ -306,10 +305,10 @@ def test_launcher_executes_exact_cli_and_fails_closed_for_running_app(self) -> N *) shift ;; esac done -printf 'cli=%s\\ncodex_home=%s\\nlab_home=%s\\nlocal_daemon=%s\\nargs=%s\\n' \\ - "$CODEX_CLI_PATH" "$CODEX_HOME" "$CODEX_LAB_HOME" \\ - "$CODEX_APP_SERVER_USE_LOCAL_DAEMON" "$args" > "$OPEN_LOG" -"$CODEX_CLI_PATH" -c features.code_mode_host=true app-server +printf 'cli=%s\\nforce_cli=%s\\ncodex_home=%s\\nlab_home=%s\\nlocal_daemon=%s\\nargs=%s\\n' \\ + "${CODEX_CLI_PATH:-}" "${CODEX_APP_SERVER_FORCE_CLI:-}" \\ + "$CODEX_HOME" "$CODEX_LAB_HOME" "$CODEX_APP_SERVER_USE_LOCAL_DAEMON" \\ + "$args" > "$OPEN_LOG" """, encoding="utf-8", ) @@ -342,7 +341,8 @@ def test_launcher_executes_exact_cli_and_fails_closed_for_running_app(self) -> N os.chmod(launcher, 0o755) environment = { **os.environ, - "CHILD_LOG": str(child_log), + "CODEX_APP_SERVER_FORCE_CLI": "1", + "CODEX_CLI_PATH": "/tmp/force-stdio", "CODEX_LAB_HOME": str(root / "codex-lab-home"), "OPEN_LOG": str(open_log), } @@ -353,6 +353,9 @@ def test_launcher_executes_exact_cli_and_fails_closed_for_running_app(self) -> N managed_cli.parent.mkdir(parents=True) managed_cli.write_bytes(embedded_cli.read_bytes()) os.chmod(managed_cli, 0o755) + managed_cli_alias = root / "managed-codex-alias" + managed_cli_alias.symlink_to(managed_cli) + environment["DAEMON_MANAGED_PATH"] = str(managed_cli_alias) completed = subprocess.run( [str(launcher)], @@ -366,15 +369,14 @@ def test_launcher_executes_exact_cli_and_fails_closed_for_running_app(self) -> N ) self.assertIn("commit=" + source_commit, completed.stderr) self.assertNotIn("executable_path", completed.stderr) - self.assertEqual( - child_log.read_text(encoding="utf-8").strip(), str(embedded_cli) - ) open_contents = open_log.read_text(encoding="utf-8") - self.assertIn(f"cli={embedded_cli}", open_contents) + self.assertIn("cli=\n", open_contents) + self.assertIn("force_cli=\n", open_contents) self.assertIn(f"codex_home={environment['CODEX_LAB_HOME']}", open_contents) self.assertIn(f"lab_home={environment['CODEX_LAB_HOME']}", open_contents) self.assertIn("local_daemon=1", open_contents) - self.assertIn(f"--env CODEX_CLI_PATH={embedded_cli}", open_contents) + self.assertIn("--env CODEX_CLI_PATH=", open_contents) + self.assertIn("--env CODEX_APP_SERVER_FORCE_CLI=", open_contents) self.assertIn( f"--env CODEX_HOME={environment['CODEX_LAB_HOME']}", open_contents ) @@ -386,7 +388,6 @@ def test_launcher_executes_exact_cli_and_fails_closed_for_running_app(self) -> N self.assertIn(str(official_app), open_contents) open_log.unlink() - child_log.unlink() environment["PROVENANCE_EXECUTABLE_PATH"] = str(root / "wrong-codex") completed = subprocess.run( [str(launcher)], diff --git a/scripts/codex_lab_package/test_live_smoke.py b/scripts/codex_lab_package/test_live_smoke.py index f9d5ee74f967..5b43f600e475 100644 --- a/scripts/codex_lab_package/test_live_smoke.py +++ b/scripts/codex_lab_package/test_live_smoke.py @@ -1,4 +1,5 @@ from pathlib import Path +import math import sys import tempfile import unittest @@ -6,13 +7,42 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from codex_lab_package.live_smoke import matching_app_server_pids +from codex_lab_package.live_smoke import desktop_transport_proof +from codex_lab_package.live_smoke import is_serving_app_server_command from codex_lab_package.live_smoke import process_has_environment from codex_lab_package.live_smoke import process_has_ancestor from codex_lab_package.live_smoke import validate_cli_provenance from codex_lab_package.live_smoke import validate_matching_build_provenance +from codex_lab_package.live_smoke import validate_timeout_seconds class LiveSmokeTest(unittest.TestCase): + def test_timeout_and_desktop_transport_proof(self) -> None: + validate_timeout_seconds(10.0) + for timeout_seconds in (9.9, math.inf, math.nan): + with self.subTest(timeout_seconds=timeout_seconds): + with self.assertRaisesRegex(ValueError, "finite and at least"): + validate_timeout_seconds(timeout_seconds) + + with tempfile.TemporaryDirectory() as temp_dir: + log_root = Path(temp_dir) + log_path = log_root / "codex-desktop-test-42-t0-i1.log" + log_path.write_text( + "Transport start success connectionId=1 hostId=local transport=websocket\n" + "initialize_handshake_result outcome=success transportKind=websocket\n", + encoding="utf-8", + ) + self.assertEqual( + desktop_transport_proof([42], log_root=log_root), + { + "desktopLogPath": str(log_path.resolve()), + "desktopTransport": "websocket", + }, + ) + log_path.write_text("stdio_transport_spawned pid=43\n", encoding="utf-8") + with self.assertRaisesRegex(RuntimeError, "bundled stdio"): + desktop_transport_proof([42], log_root=log_root) + def test_provenance_contract(self) -> None: with tempfile.TemporaryDirectory() as temp_dir: cli_path = Path(temp_dir) / "codex-lab" @@ -74,12 +104,15 @@ def test_process_and_launcher_proof_helpers(self) -> None: (10, 1, "/Applications/ChatGPT.app/Contents/Resources/codex app-server"), (20, 1, "/Applications/ChatGPT.app/Contents/MacOS/ChatGPT"), (21, 20, f"{cli_path} -c features.code_mode_host=true app-server"), + (22, 20, f"{cli_path} app-server daemon version"), ] paths = { 10: Path("/Applications/ChatGPT.app/Contents/Resources/codex"), 21: cli_path.resolve(), } self.assertEqual(matching_app_server_pids(rows, cli_path, paths.get), [21]) + self.assertTrue(is_serving_app_server_command(rows[2][2])) + self.assertFalse(is_serving_app_server_command(rows[3][2])) self.assertTrue(process_has_ancestor(21, {20}, rows)) process = ( "/Applications/ChatGPT.app/Contents/MacOS/ChatGPT " @@ -102,3 +135,19 @@ def test_process_and_launcher_proof_helpers(self) -> None: lambda _pid: process, ) ) + self.assertTrue( + process_has_environment( + 20, + {"CODEX_HOME": "/tmp/lab"}, + lambda _pid: process, + forbidden={"CODEX_CLI_PATH"}, + ) + ) + self.assertFalse( + process_has_environment( + 20, + {"CODEX_HOME": "/tmp/lab"}, + lambda _pid: process + " CODEX_CLI_PATH=/tmp/codex", + forbidden={"CODEX_CLI_PATH"}, + ) + )