Skip to content
Open
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
31 changes: 0 additions & 31 deletions Resources/bin/cmux-codex-wrapper
Original file line number Diff line number Diff line change
Expand Up @@ -615,37 +615,6 @@ export CMUX_AGENT_LAUNCH_CWD="$PWD"
[[ -n "$cmux_codex_argv_b64" ]] && export CMUX_AGENT_LAUNCH_ARGV_B64="$cmux_codex_argv_b64"
}

# On a FRESH launch, no wrapper-fired session-start: codex's own injected
# SessionStart hook (below) fires within ~1s carrying codex's REAL session_id, so
# it is the single authoritative signal. A pre-exec wrapper-fired session-start
# there had no session id and minted a junk `fallback-*` record (a phantom in the
# GUI), so it stays omitted for fresh launches.
#
# RESUME is different: codex does NOT fire SessionStart when resuming, so the
# injected hooks alone would never re-bind the session (its only pid-refreshing
# event never arrives), leaving it stuck on the dead pre-relaunch pid -> `.ended`
# -> read-only with no input bar, with no way out (you cannot submit a prompt
# from a GUI that has no composer). The wrapper has the resumed id (from argv) and
# the new live pid ($$, exported as CMUX_CODEX_PID above), so it fires the
# session-start ITSELF. The handler binds surface/workspace/cwd from the cmux env
# and pid from CMUX_CODEX_PID, re-binding the resumed session to its live pid and
# flipping it back to idle/editable. The short foreground call only admits the
# event to cmux's ordered queue; best-effort failure can never break `codex`.
cmux_codex_resume_sid="$(cmux_codex_resume_session_id "$@")"
if [[ -n "$cmux_codex_resume_sid" \
&& -n "$CMUX_CODEX_HOOK_CMUX_BIN" && -x "$CMUX_CODEX_HOOK_CMUX_BIN" ]]; then
cmux_codex_resume_payload="{\"session_id\":\"$cmux_codex_resume_sid\",\"cwd\":\"$PWD\"}"
if [[ -n "${CMUX_SOCKET_PATH:-}" ]]; then
printf '%s' "$cmux_codex_resume_payload" \
| CMUXTERM_CLI_RESPONSE_TIMEOUT_SEC=1 "$CMUX_CODEX_HOOK_CMUX_BIN" \
--socket "$CMUX_SOCKET_PATH" hooks enqueue codex session-start >/dev/null 2>&1 || true
else
printf '%s' "$cmux_codex_resume_payload" \
| CMUXTERM_CLI_RESPONSE_TIMEOUT_SEC=1 "$CMUX_CODEX_HOOK_CMUX_BIN" \
hooks enqueue codex session-start >/dev/null 2>&1 || true
fi
fi

# Build the per-invocation [hooks] injection. The cmux CLI emits the exact arg
# list (NUL-separated). Non-decision commands perform a bounded foreground
# admission to cmux's app-owned ordered queue, so Codex never waits for the
Expand Down
19 changes: 17 additions & 2 deletions tests/test_codex_wrapper_resume_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ def run_wrapper(
test_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
test_socket.bind(str(socket_path))

env = os.environ.copy()
# Exercise hook startup independently of the caller's cmux/CUA setup.
env = {key: value for key, value in os.environ.items() if not key.startswith("CMUX_")}
env["PATH"] = f"{wrapper_dir}:{real_dir}:{env.get('PATH', '/usr/bin:/bin')}"
env["HOME"] = str(tmp / "home")
env["CMUX_SURFACE_ID"] = "11111111-1111-1111-1111-111111111111"
Expand Down Expand Up @@ -168,6 +169,7 @@ def assert_session_entrypoint_is_instrumented(
restore_token=restore_token,
)
expect(code == 0, f"{label}: wrapper exited {code}: {stderr}", failures)
expect(not stderr, f"{label}: unexpected startup stderr: {stderr}", failures)
expect(real_argv[:3] == ["--enable", "hooks", "--dangerously-bypass-hook-trust"],
f"{label}: missing injected hook prefix: {real_argv}", failures)
expect(any(arg.startswith("hooks.SessionStart=") for arg in real_argv),
Expand All @@ -180,7 +182,8 @@ def assert_session_entrypoint_is_instrumented(
f"{label}: wrapper never requested local hook args: {cmux_log}", failures)
expect(not any("ping" in line for line in cmux_log),
f"{label}: transient socket health must not decide session instrumentation: {cmux_log}", failures)
expect(not any("hooks codex session-start" in line for line in cmux_log),
expect(not any("hooks codex session-start" in line or
"hooks enqueue codex session-start" in line for line in cmux_log),
f"{label}: wrapper must not synthesize SessionStart from argv: {cmux_log}", failures)
expect(observed_env.get("CMUX_CODEX_PID") not in {None, "", "__UNSET__"},
f"{label}: missing Codex process identity: {observed_env}", failures)
Expand Down Expand Up @@ -246,6 +249,16 @@ def test_stale_socket_fresh_launch_is_instrumented(failures: list[str]) -> None:
)


def test_yolo_launch_is_quiet_and_instrumented(failures: list[str]) -> None:
for socket_state in ("missing", "stale", "live"):
assert_session_entrypoint_is_instrumented(
socket_state=socket_state,
argv=["--yolo"],
label=f"yolo/{socket_state}",
failures=failures,
)


def test_restore_tokens_do_not_gate_instrumentation(failures: list[str]) -> None:
for token in (
f"claude:{SESSION_ID}",
Expand All @@ -268,6 +281,7 @@ def test_injection_failure_preserves_cmux_context(failures: list[str]) -> None:
inject_args_available=False,
)
expect(code == 0, f"inject-failure: wrapper exited {code}: {stderr}", failures)
expect(not stderr, f"inject-failure: unexpected startup stderr: {stderr}", failures)
expect(real_argv == ["resume"], f"inject-failure: original argv changed: {real_argv}", failures)
expect(any("hooks codex inject-args" in line for line in cmux_log),
f"inject-failure: injection was never attempted: {cmux_log}", failures)
Expand Down Expand Up @@ -297,6 +311,7 @@ def main() -> int:
test_direct_fork_is_instrumented(failures)
test_explicit_disable_still_bypasses_hooks(failures)
test_stale_socket_fresh_launch_is_instrumented(failures)
test_yolo_launch_is_quiet_and_instrumented(failures)
test_restore_tokens_do_not_gate_instrumentation(failures)
test_injection_failure_preserves_cmux_context(failures)
test_non_session_command_still_bypasses_hooks(failures)
Expand Down
Loading