alishahryar1--free-claude-code
b323b45434
## Problem\n\nNested Codex launches inherited the parent task's thread, shell, permission, and origin identity. This could attach a new client or compatibility probe to the parent task, hang startup, or decode probe output with the wrong Windows locale.\n\n## Changes\n\n| Before | After |\n| --- | --- |\n| cc-codex forwarded parent-only Codex task context. | cc-codex removes parent-only task context while preserving user configuration such as CODEX_HOME. |\n| The installed-Codex probe inherited the parent task and user Codex home. | The probe runs with a standalone task identity and isolated Codex home. |\n| The probe decoded output with the platform default encoding. | The probe decodes output as UTF-8 with safe replacement. | <!-- greptile_comment --> <details open><summary><h3>Greptile Summary</h3></summary> This PR isolates nested Codex launches from parent task identity. The main changes are: - Strips Codex thread, shell, permission, and origin context from launched child environments. - Preserves `CODEX_HOME` as durable user configuration. - Runs the installed Codex catalog probe with an isolated home and cleaned task context. - Decodes probe output as UTF-8 with replacement. - Updates architecture notes and bumps the package version to `3.5.3`. </details> <h3>Confidence Score: 5/5</h3> This looks safe to merge. No blocking issues found in the changed code. No files need attention. <details><summary><h3><a href="https://www.greptile.com/trex"><img alt="T-Rex" src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg" height="20" align="absmiddle"></a> T-Rex Logs</h3></summary> **What T-Rex did** - Logged the proof with exact commands, the working directory, timestamps, and test and lint summaries, including exit codes. - The first pytest capture showed tests passed but the shell wrapper exited with code 2 because /bin/sh does not support ${PIPESTATUS\[0\]}. - Re-ran the proof under bash, and the run exited with code 0. - Artifacts containing the three proof logs were created for review. <a href="https://app.greptile.com/trex/runs/14092911/artifacts"><picture><source media="(prefers-color-scheme: dark)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifactsDark.svg?v=4"><source media="(prefers-color-scheme: light)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=4"><img alt="View all artifacts" src="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=4"></picture></a> <sub><a href="https://www.greptile.com/trex"><img alt="T-Rex" src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg" height="14" align="absmiddle"></a> Ran code and verified through T-Rex</sub> </details> <details open><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | src/free_claude_code/cli/launchers/codex.py | Strips parent-only Codex context variables from the child launcher environment while keeping `CODEX_HOME`. | | tests/cli/test_entrypoints.py | Adds launcher coverage for stripped Codex task variables and preserved `CODEX_HOME`. | | tests/cli/test_codex_model_catalog.py | Runs the installed Codex catalog probe with a cleaned environment, isolated home, and UTF-8 replacement decoding. | | ARCHITECTURE.md | Documents the Codex launcher context-isolation behavior. | | pyproject.toml | Bumps the project version to `3.5.3`. | | uv.lock | Updates the locked editable package version to `3.5.3`. | </details> <sub>Reviews (3): Last reviewed commit: ["Document Codex home ownership"](https://github.com/alishahryar1/free-claude-code/commit/2140315b1f33c4162e74ca8f816228f50c38c0a1) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=43513448)</sub> <!-- /greptile_comment -->
185 行
5.2 KiB
Python
185 行
5.2 KiB
Python
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
from collections.abc import Mapping
|
|
from pathlib import Path
|
|
from typing import Any, cast
|
|
|
|
import pytest
|
|
|
|
from free_claude_code.cli.launchers.codex_model_catalog import (
|
|
build_codex_model_catalog,
|
|
write_codex_model_catalog,
|
|
)
|
|
|
|
|
|
def _models_payload(*model_ids: str) -> dict[str, Any]:
|
|
return {
|
|
"data": [
|
|
{
|
|
"id": model_id,
|
|
"display_name": model_id.replace("anthropic/", ""),
|
|
}
|
|
for model_id in model_ids
|
|
]
|
|
}
|
|
|
|
|
|
def _catalog_models(catalog: Mapping[str, Any]) -> list[Mapping[str, Any]]:
|
|
models = catalog["models"]
|
|
assert isinstance(models, list)
|
|
catalog_models: list[Mapping[str, Any]] = []
|
|
for model in models:
|
|
assert isinstance(model, Mapping)
|
|
catalog_models.append(cast(Mapping[str, Any], model))
|
|
return catalog_models
|
|
|
|
|
|
def _slugs(catalog: Mapping[str, Any]) -> list[str]:
|
|
slugs: list[str] = []
|
|
for model in _catalog_models(catalog):
|
|
slug = model["slug"]
|
|
assert isinstance(slug, str)
|
|
slugs.append(slug)
|
|
return slugs
|
|
|
|
|
|
def test_codex_catalog_converts_configured_and_cached_models_to_direct_slugs() -> None:
|
|
catalog = build_codex_model_catalog(
|
|
_models_payload(
|
|
"anthropic/nvidia_nim/nvidia/nemotron-3-super",
|
|
"claude-3-freecc-no-thinking/nvidia_nim/nvidia/nemotron-3-super",
|
|
"anthropic/open_router/meta-llama/llama-3.3-70b",
|
|
"claude-3-freecc-no-thinking/open_router/meta-llama/llama-3.3-70b",
|
|
)
|
|
)
|
|
|
|
assert _slugs(catalog) == [
|
|
"nvidia_nim/nvidia/nemotron-3-super",
|
|
"open_router/meta-llama/llama-3.3-70b",
|
|
]
|
|
model = _catalog_models(catalog)[0]
|
|
assert {
|
|
"slug",
|
|
"display_name",
|
|
"description",
|
|
"default_reasoning_level",
|
|
"supported_reasoning_levels",
|
|
"shell_type",
|
|
"visibility",
|
|
"supported_in_api",
|
|
"priority",
|
|
"additional_speed_tiers",
|
|
"service_tiers",
|
|
} <= set(model)
|
|
|
|
|
|
def test_codex_catalog_excludes_claude_compatibility_model_ids() -> None:
|
|
catalog = build_codex_model_catalog(
|
|
_models_payload(
|
|
"claude-opus-4-20250514",
|
|
"claude-3-haiku-20240307",
|
|
"anthropic/nvidia_nim/provider-model",
|
|
)
|
|
)
|
|
|
|
assert _slugs(catalog) == ["nvidia_nim/provider-model"]
|
|
|
|
|
|
def test_codex_catalog_skips_no_thinking_duplicate_when_normal_slug_exists() -> None:
|
|
catalog = build_codex_model_catalog(
|
|
_models_payload(
|
|
"claude-3-freecc-no-thinking/nvidia_nim/provider-model",
|
|
"anthropic/nvidia_nim/provider-model",
|
|
)
|
|
)
|
|
|
|
assert _slugs(catalog) == ["nvidia_nim/provider-model"]
|
|
|
|
|
|
def test_codex_catalog_preserves_no_thinking_only_entries_for_routing() -> None:
|
|
catalog = build_codex_model_catalog(
|
|
_models_payload("claude-3-freecc-no-thinking/open_router/plain-model")
|
|
)
|
|
|
|
assert _slugs(catalog) == ["claude-3-freecc-no-thinking/open_router/plain-model"]
|
|
|
|
|
|
def test_codex_catalog_ordering_and_priorities_are_deterministic() -> None:
|
|
catalog = build_codex_model_catalog(
|
|
_models_payload(
|
|
"anthropic/gemini/models/gemini-test",
|
|
"anthropic/nvidia_nim/nvidia/test",
|
|
"anthropic/gemini/models/gemini-test",
|
|
"anthropic/open_router/provider/test",
|
|
)
|
|
)
|
|
|
|
models = _catalog_models(catalog)
|
|
assert _slugs(catalog) == [
|
|
"gemini/models/gemini-test",
|
|
"nvidia_nim/nvidia/test",
|
|
"open_router/provider/test",
|
|
]
|
|
assert [model["priority"] for model in models] == [0, 1, 2]
|
|
|
|
|
|
def test_codex_catalog_accepts_future_direct_provider_slugs() -> None:
|
|
catalog = build_codex_model_catalog(
|
|
_models_payload(
|
|
"nvidia_nim/provider-model",
|
|
"anthropic/open_router/provider-model",
|
|
)
|
|
)
|
|
|
|
assert _slugs(catalog) == [
|
|
"nvidia_nim/provider-model",
|
|
"open_router/provider-model",
|
|
]
|
|
|
|
|
|
def test_generated_catalog_schema_is_accepted_by_installed_codex(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
codex_binary = shutil.which("codex")
|
|
if codex_binary is None:
|
|
pytest.skip("Codex CLI is not installed")
|
|
|
|
catalog_path = tmp_path / "codex-model-catalog.json"
|
|
write_codex_model_catalog(
|
|
catalog_path,
|
|
build_codex_model_catalog(_models_payload("anthropic/nvidia_nim/test-model")),
|
|
)
|
|
codex_home = tmp_path / "codex-home"
|
|
codex_home.mkdir()
|
|
codex_env = os.environ.copy()
|
|
for key in (
|
|
"CODEX_THREAD_ID",
|
|
"CODEX_INTERNAL_ORIGINATOR_OVERRIDE",
|
|
"CODEX_SHELL",
|
|
"CODEX_PERMISSION_PROFILE",
|
|
):
|
|
codex_env.pop(key, None)
|
|
codex_env["CODEX_HOME"] = str(codex_home)
|
|
|
|
result = subprocess.run(
|
|
[
|
|
codex_binary,
|
|
"debug",
|
|
"models",
|
|
"-c",
|
|
f"model_catalog_json={json.dumps(str(catalog_path))}",
|
|
],
|
|
capture_output=True,
|
|
check=False,
|
|
encoding="utf-8",
|
|
env=codex_env,
|
|
errors="replace",
|
|
text=True,
|
|
timeout=10,
|
|
)
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
assert "nvidia_nim/test-model" in result.stdout
|