mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
sdk/python: add first-class login support (#23093)
## 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.
This commit is contained in:
committed by
GitHub
Unverified
parent
0445b290fe
commit
4c89772314
@@ -0,0 +1,90 @@
|
||||
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}",
|
||||
}
|
||||
Reference in New Issue
Block a user