-
Notifications
You must be signed in to change notification settings - Fork 1
fix: Codex CLI OAuth device flow fails in headless/SSH environments #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -243,6 +243,70 @@ or GitHub token for your chosen provider. | |
|
|
||
| --- | ||
|
|
||
| ## 6. Codex CLI in headless / SSH environments | ||
|
|
||
| When `ENABLE_AGENT_CODEX=true` is set and the server is provisioned via | ||
| `cloud-init` or `install.sh`, there is no interactive terminal available for | ||
| browser-based OAuth flows. The Codex CLI would hang waiting for user input | ||
| if no credential is pre-configured. | ||
|
|
||
| ### How authentication is resolved (in order) | ||
|
|
||
| | Priority | Credential | How to obtain | | ||
| |----------|------------|---------------| | ||
| | 1 (best) | `CODEX_OPENAI_AUTH_CODE` | Run the configurator before provisioning and complete the OpenAI Device Flow there. The code is embedded in the generated config. | | ||
| | 2 | `OPENAI_API_KEY` | Create a key at <https://platform.openai.com/api-keys> | | ||
| | 3 | `GITHUB_TOKEN` | GitHub OAuth Device Flow (same as Copilot) | | ||
| | — | None set | `agents.sh` prints a warning and skips; workspace startup script prints the Device Flow URL | | ||
|
|
||
| ### Path 1 — pre-obtain the auth code with the configurator (recommended) | ||
|
|
||
| ```bash | ||
| # On your local machine, before provisioning: | ||
| python -m configurator | ||
| # → Enable Codex | ||
| # → Choose "Sign in with ChatGPT (Device Flow)" | ||
| # → Complete auth in browser | ||
| # The code is saved as CODEX_OPENAI_AUTH_CODE in cloud-init.yaml / RVSconfig.yml | ||
| ``` | ||
|
|
||
| At workspace start, the startup script runs: | ||
| ```bash | ||
| codex login --auth-code "$CODEX_OPENAI_AUTH_CODE" | ||
| ``` | ||
| This completes the PKCE exchange non-interactively — no browser needed on the | ||
| server side. | ||
|
|
||
| ### Path 2 — API key | ||
|
|
||
| Add to `/etc/dev-server/env`: | ||
| ``` | ||
| OPENAI_API_KEY=sk-... | ||
| ``` | ||
|
|
||
|
Comment on lines
+280
to
+286
|
||
| ### Path 3 — post-provisioning manual auth | ||
|
|
||
| If the workspace is already running, open a terminal inside it and run: | ||
| ```bash | ||
| codex login --device-auth | ||
| # Follow the URL that is printed: | ||
| # https://auth.openai.com/codex/device | ||
| ``` | ||
|
|
||
| ### Reading the Device Flow URL from the log | ||
|
|
||
| When no credential is set at provisioning time, `agents.sh` logs the following | ||
| (visible in `/var/log/dev-server-provision.log`): | ||
|
|
||
| ``` | ||
| [agents] WARN: Device Flow URL (for manual auth): https://auth.openai.com/codex/device | ||
| ``` | ||
|
|
||
| The workspace startup script also echoes the URL on first start so it appears | ||
| in the Coder workspace log panel. | ||
|
|
||
| --- | ||
|
|
||
| ## Adding a new provider | ||
|
|
||
| To add OAuth for a new AI agent: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ set -euo pipefail | |
|
|
||
| LOG_FILE="${LOG_FILE:-/var/log/dev-server-provision.log}" | ||
| log() { echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] [agents] $*" | tee -a "$LOG_FILE"; } | ||
| warn() { log "WARN: $*"; } | ||
|
|
||
| ENV_FILE="${ENV_FILE:-/etc/dev-server/env}" | ||
| AGENT_ENV_FILE="/etc/dev-server/agent-env" | ||
|
|
@@ -26,6 +27,12 @@ if [[ ! -f "$ENV_FILE" ]]; then | |
| exit 0 | ||
| fi | ||
|
|
||
| # Source the env file so we can inspect individual variable values below. | ||
| set -a | ||
| # shellcheck source=/dev/null | ||
| source "$ENV_FILE" | ||
| set +a | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Extract agent-relevant keys only | ||
| # --------------------------------------------------------------------------- | ||
|
|
@@ -49,3 +56,57 @@ chmod 0644 "$AGENT_ENV_FILE" | |
| log "Agent key file written ($(wc -l < "$AGENT_ENV_FILE") lines)." | ||
| log "Note: AI agent CLIs are installed inside the workspace Docker image." | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Codex CLI — credential validation | ||
| # --------------------------------------------------------------------------- | ||
| # Codex CLI authenticates via one of three methods (checked in order): | ||
| # 1. CODEX_OPENAI_AUTH_CODE — pre-obtained OpenAI Device Flow auth code | ||
| # (obtained via the configurator or `codex login --device-auth`). | ||
| # The workspace startup script will exchange it non-interactively. | ||
| # 2. OPENAI_API_KEY — direct API key (billing required). | ||
| # 3. GITHUB_TOKEN — Codex can authenticate via GitHub OAuth. | ||
| # | ||
| # If none of the above are set, Codex would try to launch an interactive | ||
| # Device Flow in the terminal — which hangs silently in headless/SSH | ||
| # environments. We warn early and skip instead of letting it hang. | ||
| # --------------------------------------------------------------------------- | ||
| if [[ "${ENABLE_AGENT_CODEX:-false}" == "true" ]]; then | ||
| if [[ -n "${CODEX_OPENAI_AUTH_CODE:-}" ]]; then | ||
| log "Codex: CODEX_OPENAI_AUTH_CODE is set — workspace startup will complete" | ||
| log " non-interactive authentication via 'codex login --auth-code'." | ||
| elif [[ -n "${OPENAI_API_KEY:-}" ]]; then | ||
|
Comment on lines
+73
to
+77
|
||
| log "Codex: OPENAI_API_KEY is set — Codex CLI will use API key mode." | ||
| elif [[ -n "${GITHUB_TOKEN:-}" ]]; then | ||
| log "Codex: GITHUB_TOKEN is set — Codex CLI will use GitHub OAuth mode." | ||
| else | ||
| warn "Codex is enabled but no authentication credential is configured." | ||
| warn "Codex CLI would hang waiting for interactive input in a headless environment." | ||
| warn "" | ||
| warn "To fix this, obtain an auth code BEFORE provisioning:" | ||
| warn " 1. Run the configurator: python -m configurator" | ||
| warn " 2. Select 'OpenAI Codex CLI' → 'Sign in with ChatGPT (Device Flow)'" | ||
| warn " 3. Authenticate in your browser — the configurator saves the code" | ||
| warn " as CODEX_OPENAI_AUTH_CODE in your cloud-init.yaml / RVSconfig.yml" | ||
| warn "" | ||
| warn "Alternatively, set one of these in /etc/dev-server/env and re-run:" | ||
| warn " CODEX_OPENAI_AUTH_CODE=<auth_code> (ChatGPT Plus/Pro plan)" | ||
| warn " OPENAI_API_KEY=sk-... (API billing plan)" | ||
| warn " GITHUB_TOKEN=ghp_... (GitHub OAuth)" | ||
| warn "" | ||
| warn "Device Flow URL (for manual auth): https://auth.openai.com/codex/device" | ||
| warn "Skipping Codex credential setup — Codex may prompt for login in the workspace." | ||
| fi | ||
| fi | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # OpenCode — log provider info | ||
| # --------------------------------------------------------------------------- | ||
| if [[ "${ENABLE_AGENT_OPENCODE:-false}" == "true" ]]; then | ||
| if [[ -n "${OPENCODE_PROVIDER:-}" ]]; then | ||
| log "OpenCode: provider(s) configured: ${OPENCODE_PROVIDER}" | ||
| else | ||
| warn "OpenCode is enabled but OPENCODE_PROVIDER is not set." | ||
| warn "OpenCode may prompt for a provider selection in the workspace." | ||
| fi | ||
| fi | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
validate_api_key_optional()currently returnsTruefor any input (including whitespace-only strings), which contradicts its docstring (“Accept empty or any non-whitespace string”) and makes the validator ineffective. Consider stripping the value and returning an error string whenvalueis non-empty butvalue.strip()is empty, and add a test case for whitespace-only input.