fix: configure Codex provider authentication

This commit is contained in:
vee
2026-09-07 18:17:10 +08:00
parent 53d80ab662
commit cf9b89508c
6 changed files with 556 additions and 29 deletions
+43 -1
View File
@@ -20,9 +20,13 @@ EXTRA_MCP_NAME = next(
None,
)
INVALID_MCP_CONFIG = "--fake-invalid-mcp-config" in sys.argv[1:]
UNAUTHENTICATED = "--fake-unauthenticated" in sys.argv[1:]
LOGIN_FAILURE = "--fake-login-failure" in sys.argv[1:]
NO_OPENAI_AUTH_REQUIRED = "--fake-no-openai-auth-required" in sys.argv[1:]
thread_number = 0
turn_number = 0
waiting_turns: dict[str, str] = {}
account_present = not UNAUTHENTICATED
ENVIRONMENT_PATH.write_text(
json.dumps(sorted(os.environ), ensure_ascii=True),
@@ -35,8 +39,14 @@ def emit(message: dict[str, object]) -> None:
def trace(message: dict[str, object]) -> None:
recorded = message
if message.get("method") == "account/login/start":
recorded = dict(message)
params = message.get("params")
if isinstance(params, dict):
recorded["params"] = {**params, "apiKey": "<redacted>"}
with TRACE_PATH.open("a", encoding="utf-8") as stream:
stream.write(json.dumps(message, ensure_ascii=False) + "\n")
stream.write(json.dumps(recorded, ensure_ascii=False) + "\n")
for raw_line in sys.stdin:
@@ -58,6 +68,38 @@ for raw_line in sys.stdin:
)
elif method == "initialized":
continue
elif method == "account/read":
emit(
{
"id": request_id,
"result": {
"account": ({"type": "apiKey"} if account_present else None),
"requiresOpenaiAuth": not NO_OPENAI_AUTH_REQUIRED,
},
}
)
elif method == "account/login/start":
if LOGIN_FAILURE:
emit(
{
"id": request_id,
"error": {
"code": -32000,
"message": f"authentication rejected: {params.get('apiKey', '')}",
},
}
)
continue
if params.get("type") != "apiKey" or not isinstance(params.get("apiKey"), str):
emit(
{
"id": request_id,
"error": {"code": -32602, "message": "invalid login request"},
}
)
continue
account_present = True
emit({"id": request_id, "result": {"type": "apiKey"}})
elif method == "config/read":
mcp_override = next(
(
+279 -3
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import json
import sys
import tomllib
import traceback
from pathlib import Path
import pytest
@@ -30,6 +31,7 @@ def _client(tmp_path: Path) -> tuple[CodexAppServerClient, Path]:
reasoning_effort="low",
request_timeout=2,
startup_timeout=2,
environment={"OPENAI_BASE_URL": "https://api.openai.com/v1"},
mcp_server_path=MCP_SERVER,
)
return client, trace_path
@@ -81,15 +83,15 @@ def test_real_stdio_adapter_handshake_stream_and_transport_overrides(tmp_path: P
wire = [json.loads(line) for line in trace_path.read_text(encoding="utf-8").splitlines()]
methods = [message.get("method") for message in wire]
assert methods[:3] == ["initialize", "initialized", "config/read"]
assert methods[:4] == ["initialize", "initialized", "account/read", "config/read"]
assert "account/login/start" not in methods
thread_request = next(item for item in wire if item.get("method") == "thread/start")
params = thread_request["params"]
assert params["sandbox"] == "read-only"
assert params["approvalPolicy"] == "never"
assert params["model"] == "gpt-test-fast"
assert params["modelProvider"] == "fake-provider"
assert params["config"]["model_providers.fake-provider.supports_websockets"] is False
assert params["config"]["model_providers.fake-provider.stream_max_retries"] == 1
assert "model_providers" not in params["config"]
turn_request = next(item for item in wire if item.get("method") == "turn/start")
assert turn_request["params"]["sandboxPolicy"] == {
"type": "readOnly",
@@ -122,6 +124,280 @@ def test_interrupt_is_sent_and_waits_for_authoritative_terminal_event(tmp_path:
assert terminal["status"] == "interrupted"
def test_startup_bootstraps_missing_builtin_account_from_api_key_without_leaking_it(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
api_key = "test-model-provider-key-not-a-secret"
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
client = CodexAppServerClient(
cwd=tmp_path,
codex_command=[
sys.executable,
"-u",
str(FAKE_SERVER),
"--fake-unauthenticated",
],
request_timeout=2,
startup_timeout=2,
environment={"OPENAI_API_KEY": api_key},
mcp_server_path=MCP_SERVER,
)
try:
client.start(CALLBACK_URL, CALLBACK_TOKEN)
status = client.status
stderr = "\n".join(client.stderr_tail)
assert client.available is True
finally:
client.close()
trace_text = (tmp_path / "wire.jsonl").read_text(encoding="utf-8")
wire = [json.loads(line) for line in trace_text.splitlines()]
methods = [message.get("method") for message in wire]
assert methods[:6] == [
"initialize",
"initialized",
"account/read",
"account/login/start",
"account/read",
"config/read",
]
login = next(item for item in wire if item.get("method") == "account/login/start")
assert login["params"] == {"type": "apiKey", "apiKey": "<redacted>"}
assert api_key not in trace_text
assert api_key not in status
assert api_key not in stderr
assert api_key not in " ".join(client._build_command())
def test_startup_fails_fast_when_builtin_account_and_api_key_are_missing(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
client = CodexAppServerClient(
cwd=tmp_path,
codex_command=[
sys.executable,
"-u",
str(FAKE_SERVER),
"--fake-unauthenticated",
],
request_timeout=2,
startup_timeout=2,
mcp_server_path=MCP_SERVER,
)
with pytest.raises(CodexAppServerUnavailable, match="authentication is not configured"):
client.start(CALLBACK_URL, CALLBACK_TOKEN)
assert client.available is False
assert client.status == "failed"
client.close()
def test_startup_sanitizes_api_key_login_failure(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
api_key = "test-rejected-provider-key-not-a-secret"
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
client = CodexAppServerClient(
cwd=tmp_path,
codex_command=[
sys.executable,
"-u",
str(FAKE_SERVER),
"--fake-unauthenticated",
"--fake-login-failure",
],
request_timeout=2,
startup_timeout=2,
environment={"OPENAI_API_KEY": api_key},
mcp_server_path=MCP_SERVER,
)
with pytest.raises(
CodexAppServerUnavailable,
match="API-key authentication failed",
) as error:
client.start(CALLBACK_URL, CALLBACK_TOKEN)
assert api_key not in str(error.value)
assert "authentication rejected" not in str(error.value)
formatted_error = "".join(traceback.format_exception(error.value))
assert api_key not in formatted_error
assert "authentication rejected" not in formatted_error
assert api_key not in "\n".join(client.stderr_tail)
assert client.available is False
assert client.status == "failed"
client.close()
def test_startup_allows_provider_that_does_not_require_openai_account(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
client = CodexAppServerClient(
cwd=tmp_path,
codex_command=[
sys.executable,
"-u",
str(FAKE_SERVER),
"--fake-unauthenticated",
"--fake-no-openai-auth-required",
],
request_timeout=2,
startup_timeout=2,
mcp_server_path=MCP_SERVER,
)
try:
client.start(CALLBACK_URL, CALLBACK_TOKEN)
assert client.available is True
finally:
client.close()
wire = [
json.loads(line)
for line in (tmp_path / "wire.jsonl").read_text(encoding="utf-8").splitlines()
]
assert "account/login/start" not in [item.get("method") for item in wire]
def test_custom_base_url_uses_complete_thread_provider_and_environment_key_only(
tmp_path: Path,
) -> None:
api_key = "test-proxy-provider-key-not-a-secret"
base_url = "http://127.0.0.1:9876/v1"
client = CodexAppServerClient(
cwd=tmp_path,
codex_command=[sys.executable, "-u", str(FAKE_SERVER)],
model="gpt-test-proxy",
request_timeout=2,
startup_timeout=2,
environment={
"OPENAI_API_KEY": api_key,
"OPENAI_BASE_URL": base_url,
},
mcp_server_path=MCP_SERVER,
)
try:
client.start(CALLBACK_URL, CALLBACK_TOKEN)
client.start_thread(ephemeral=True)
assert client.available is True
finally:
client.close()
trace_text = (tmp_path / "wire.jsonl").read_text(encoding="utf-8")
wire = [json.loads(line) for line in trace_text.splitlines()]
methods = [item.get("method") for item in wire]
assert "account/read" not in methods
assert "account/login/start" not in methods
request = next(item for item in wire if item.get("method") == "thread/start")
assert request["params"]["modelProvider"] == "n7web_openai_compatible"
assert request["params"]["model"] == "gpt-test-proxy"
provider = request["params"]["config"]["model_providers"]["n7web_openai_compatible"]
assert provider == {
"name": "N7Web OpenAI-compatible",
"base_url": base_url,
"env_key": "OPENAI_API_KEY",
"wire_api": "responses",
"requires_openai_auth": False,
"supports_websockets": False,
"stream_max_retries": 1,
"request_max_retries": 1,
}
assert api_key not in trace_text
assert api_key not in " ".join(client._build_command())
def test_custom_base_url_fails_fast_without_its_environment_key(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
client = CodexAppServerClient(
cwd=tmp_path,
codex_command=[sys.executable, "-u", str(FAKE_SERVER)],
request_timeout=2,
startup_timeout=2,
environment={"OPENAI_BASE_URL": "http://127.0.0.1:9876/v1"},
mcp_server_path=MCP_SERVER,
)
with pytest.raises(
CodexAppServerUnavailable,
match="custom model provider authentication is not configured",
):
client.start(CALLBACK_URL, CALLBACK_TOKEN)
assert client.status == "failed"
assert client.available is False
client.close()
def test_explicit_environment_controls_model_and_proxy_provider(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("CODEX_ASSISTANT_MODEL", "ambient-model-must-not-win")
monkeypatch.setenv("CODEX_ASSISTANT_MODEL_PROVIDER", "ambient-provider-must-not-win")
monkeypatch.setenv("OPENAI_BASE_URL", "https://ambient.example.invalid/v1")
client = CodexAppServerClient(
cwd=tmp_path,
codex_command=[sys.executable, "-u", str(FAKE_SERVER)],
request_timeout=2,
startup_timeout=2,
environment={
"codex_assistant_model": "environment-model",
"codex_assistant_model_provider": "environment-provider",
"openai_api_key": "environment-provider-key-not-secret",
"openai_base_url": "http://localhost:9988/v1/",
},
mcp_server_path=MCP_SERVER,
)
try:
client.start(CALLBACK_URL, CALLBACK_TOKEN)
client.start_thread(ephemeral=True)
finally:
client.close()
wire = [
json.loads(line)
for line in (tmp_path / "wire.jsonl").read_text(encoding="utf-8").splitlines()
]
request = next(item for item in wire if item.get("method") == "thread/start")
assert request["params"]["model"] == "environment-model"
assert request["params"]["modelProvider"] == "n7web_openai_compatible"
provider = request["params"]["config"]["model_providers"]["n7web_openai_compatible"]
assert provider["base_url"] == "http://localhost:9988/v1"
@pytest.mark.parametrize(
"base_url",
[
"http://proxy.example.test/v1",
"https://user:password@proxy.example.test/v1",
"https://proxy.example.test/v1?api_key=not-allowed",
"file:///tmp/model",
],
)
def test_model_base_url_rejects_insecure_or_credential_bearing_values(
tmp_path: Path,
base_url: str,
) -> None:
with pytest.raises(ValueError, match="OPENAI_BASE_URL"):
CodexAppServerClient(
cwd=tmp_path,
codex_command=[sys.executable, "-u", str(FAKE_SERVER)],
environment={
"OPENAI_API_KEY": "test-provider-key-not-secret",
"OPENAI_BASE_URL": base_url,
},
mcp_server_path=MCP_SERVER,
)
def test_process_exit_during_turn_fails_the_handle_immediately(tmp_path: Path) -> None:
client, _trace_path = _client(tmp_path)
try: