diff --git a/docs/reference/trajectory-assessment.md b/docs/reference/trajectory-assessment.md index 64e41d4b0c..4547a86106 100644 --- a/docs/reference/trajectory-assessment.md +++ b/docs/reference/trajectory-assessment.md @@ -24,7 +24,11 @@ not independent verification of a model's translation or judgments. The Codex observer reuses native Chat's shared authenticated HTTP transport. Credentials and refresh remain with Codex's existing signed-in account; the observer neither copies tokens nor imports user tool/rule configuration. Its -explicit read-only sandbox, disabled shell/exec/web tools and fixed supplied +child follows the desktop service's explicit `LOOPX_CHAT_CODEX_HOME` account +selection rather than an unrelated inherited `CODEX_HOME`. Without that Chat +selection, ordinary CLI account discovery remains unchanged. This selects an +existing native account directory; it does not copy credentials or change login. +The explicit read-only sandbox, disabled shell/exec/web tools and fixed supplied evidence remain in force. A failed provider attempt stays in job history and requires an explicit retry after the host problem is repaired. diff --git a/loopx/capabilities/benchmark_toolkit/trajectory_assessment.py b/loopx/capabilities/benchmark_toolkit/trajectory_assessment.py index e2d68e2468..f0c8e7c9a6 100644 --- a/loopx/capabilities/benchmark_toolkit/trajectory_assessment.py +++ b/loopx/capabilities/benchmark_toolkit/trajectory_assessment.py @@ -156,6 +156,12 @@ def execute( # rules. The shared host transport delegates credentials/refresh to # Codex; it supplies no endpoint, bearer token or account configuration. transport = shared_chatgpt_transport({}, {}) + # The desktop service pins its Chat account independently of an + # unrelated terminal's CODEX_HOME. Use that existing host selection; + # leave ordinary CLI environments and Codex-owned credentials intact. + environment = dict(os.environ) + if chat_home := environment.get("LOOPX_CHAT_CODEX_HOME"): + environment["CODEX_HOME"] = chat_home provider = transport["model_provider"] transport_args = ["-c", f"model_provider={json.dumps(provider)}"] for key, value in transport["model_providers"][provider].items(): @@ -237,6 +243,7 @@ def execute( stdin=prompt, stdout=events, stderr=errors, + env=environment, text=True, encoding="utf-8", ) diff --git a/tests/capabilities/test_trajectory_assessment.py b/tests/capabilities/test_trajectory_assessment.py index d19728c4f0..3588fc2c98 100644 --- a/tests/capabilities/test_trajectory_assessment.py +++ b/tests/capabilities/test_trajectory_assessment.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import os from pathlib import Path import sys import threading @@ -64,7 +65,8 @@ def prepared(tmp_path): def provider( - tmp_path: Path, *, delay=0, code=0, invalid=False, invalid_utf8=False + tmp_path: Path, *, delay=0, code=0, invalid=False, invalid_utf8=False, + expected_home=None, ) -> str: script = tmp_path / "codex-fixture" output = { @@ -88,7 +90,7 @@ def provider( ) script.write_text( f"""#!{sys.executable} -import json,sys,time +import json,os,sys,time from pathlib import Path assert "\\u539f\\u59cb\\u76ee\\u6807" in sys.stdin.buffer.read().decode("utf-8") options = dict(arg.split('=', 1) for index, arg in enumerate(sys.argv) if index > 0 and sys.argv[index-1] == '-c') @@ -100,6 +102,8 @@ def provider( assert sys.argv[sys.argv.index('--sandbox')+1] == 'read-only' assert json.loads(options['web_search']) == 'disabled' assert 'shell_tool' in sys.argv and 'unified_exec' in sys.argv +if {expected_home!r} is not None: + assert os.environ.get('CODEX_HOME') == {expected_home!r} time.sleep({delay}) if {code}: sys.exit({code}) Path(sys.argv[sys.argv.index('-o')+1]).write_bytes({payload!r}) @@ -112,6 +116,26 @@ def provider( return str(script) +@pytest.mark.parametrize("chat_home", [None, "", "selected-chat-home"]) +def test_actual_child_uses_explicit_chat_account_not_unrelated_terminal( + prepared, tmp_path, monkeypatch, chat_home +): + monkeypatch.setenv("CODEX_HOME", "unrelated-terminal-home") + if chat_home is None: + monkeypatch.delenv("LOOPX_CHAT_CODEX_HOME", raising=False) + else: + monkeypatch.setenv("LOOPX_CHAT_CODEX_HOME", chat_home) + expected = "selected-chat-home" if chat_home else "unrelated-terminal-home" + result = execute( + prepared, tmp_path / "jobs", + codex=provider(tmp_path, expected_home=expected), + ) + assert result["status"] == "succeeded" + # Selection belongs to this child, not the server's process or saved input. + assert os.environ["CODEX_HOME"] == "unrelated-terminal-home" + assert "CODEX_HOME" not in result["config"] + + def test_actual_child_reuse_and_repair_of_derived_output(prepared, tmp_path): first = execute(prepared, tmp_path / "jobs", codex=provider(tmp_path)) assert first["status"] == "succeeded" and first["usage"] == {