fix: configure Codex provider authentication
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user