mirror of
https://github.com/openai/codex.git
synced 2026-08-23 13:09:46 +00:00
## Why The Python SDK can already create threads and run turns, but authentication still has to be arranged outside the SDK. App-server already exposes account login, account inspection, logout, and `account/login/completed` notifications, so SDK users currently have to work around a missing public client layer for a core setup step. This change makes authentication a normal SDK workflow while preserving the backend flow shape: API-key login completes immediately, and interactive ChatGPT flows return live handles that complete later through app-server notifications. ## What changed - Added public sync and async auth methods on `Codex` / `AsyncCodex`: - `login_api_key(...)` - `login_chatgpt()` - `login_chatgpt_device_code()` - `account(...)` - `logout()` - Added public browser-login and device-code handle types with attempt-local `wait()` and `cancel()` helpers. Cancellation stays on the handle instead of a root-level SDK method. - Extended the Python app-server client and notification router so login completion events are routed by `login_id` without consuming unrelated global notifications. - Kept login request/handle logic in a focused internal `_login.py` module so `api.py` remains the public facade instead of absorbing more auth plumbing. - Exported the new handle types plus curated account/login response types from the SDK surfaces. - Updated SDK docs, added sync/async login walkthrough examples, and added a notebook login walkthrough cell. ## Verification Added SDK coverage for: - API-key login, account readback, and logout through the app-server harness in both sync and async clients. - Browser login cancellation plus `handle.wait()` completion through the real app-server boundary used by the Python SDK harness. - Waiter routing that stays scoped across replaced interactive login attempts, plus async handle cancellation coverage. - Login notification demuxing, replay of early completion events, and async client delegation. - Public export/signature assertions. - Real integration-suite smoke coverage for the new examples and notebook login cell.
91 lines
3.3 KiB
Python
91 lines
3.3 KiB
Python
from __future__ import annotations
|
|
|
|
import base64
|
|
import json
|
|
|
|
from app_server_harness import AppServerHarness
|
|
|
|
from openai_codex import AppServerConfig, Codex
|
|
from openai_codex.generated.v2_all import (
|
|
ChatgptAuthTokensLoginAccountParams,
|
|
LoginAccountParams,
|
|
)
|
|
|
|
|
|
def _app_server_config(harness: AppServerHarness) -> AppServerConfig:
|
|
"""Build an isolated login config without inheriting ambient API-key auth."""
|
|
config = harness.app_server_config()
|
|
config.env = {**(config.env or {}), "OPENAI_API_KEY": ""}
|
|
return config
|
|
|
|
|
|
def test_api_key_login_authenticates_follow_up_model_requests(tmp_path) -> None:
|
|
"""API-key login should authorize the next Responses request with that key."""
|
|
with AppServerHarness(tmp_path, requires_openai_auth=True) as harness:
|
|
harness.responses.enqueue_assistant_message("api key auth", response_id="api-key-auth")
|
|
|
|
with Codex(config=_app_server_config(harness)) as codex:
|
|
codex.login_api_key("sk-sdk-login-test")
|
|
result = codex.thread_start().run("prove api key auth")
|
|
request = harness.responses.single_request()
|
|
|
|
assert {
|
|
"final_response": result.final_response,
|
|
"authorization": request.header("authorization"),
|
|
} == {
|
|
"final_response": "api key auth",
|
|
"authorization": "Bearer sk-sdk-login-test",
|
|
}
|
|
|
|
|
|
def test_chatgpt_token_login_authenticates_follow_up_model_requests(tmp_path) -> None:
|
|
"""ChatGPT token handoff should authorize later Responses requests with that token."""
|
|
account_id = "workspace-sdk-chatgpt"
|
|
|
|
def _encode(payload: dict[str, object]) -> str:
|
|
raw = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")
|
|
return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii")
|
|
|
|
# App-server parses claims from the access token before persisting external ChatGPT auth.
|
|
header = _encode({"alg": "none", "typ": "JWT"})
|
|
claims = _encode(
|
|
{
|
|
"email": "sdk-chatgpt@example.com",
|
|
"https://api.openai.com/auth": {
|
|
"chatgpt_account_id": account_id,
|
|
"chatgpt_plan_type": "pro",
|
|
},
|
|
}
|
|
)
|
|
access_token = f"{header}.{claims}.sig"
|
|
|
|
with AppServerHarness(tmp_path, requires_openai_auth=True) as harness:
|
|
harness.responses.enqueue_assistant_message(
|
|
"chatgpt token auth",
|
|
response_id="chatgpt-token-auth",
|
|
)
|
|
|
|
with Codex(config=_app_server_config(harness)) as codex:
|
|
login = codex._client.account_login_start(
|
|
LoginAccountParams(
|
|
root=ChatgptAuthTokensLoginAccountParams(
|
|
access_token=access_token,
|
|
chatgpt_account_id=account_id,
|
|
chatgpt_plan_type="pro",
|
|
type="chatgptAuthTokens",
|
|
)
|
|
)
|
|
)
|
|
result = codex.thread_start().run("prove chatgpt token auth")
|
|
request = harness.responses.single_request()
|
|
|
|
assert {
|
|
"login_type": login.root.type,
|
|
"final_response": result.final_response,
|
|
"authorization": request.header("authorization"),
|
|
} == {
|
|
"login_type": "chatgptAuthTokens",
|
|
"final_response": "chatgpt token auth",
|
|
"authorization": f"Bearer {access_token}",
|
|
}
|