mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
feat(core/auth): prefer ChatGPT over API key when tokens exist
- Default and conversation startup to - Honor explicit API-key preference only when requested feat(arg0/env): ignore provider keys in project .env - Load ~/.code/.env first - Always ignore OPENAI_API_KEY/AZURE_OPENAI_API_KEY in local .env feat(tui): show bold "Auth: API key" badge when not using ChatGPT feat(tui/onboarding): set Never + DangerFullAccess on trust docs(auth): document .env safety and UI indicator refactor(core/token_data): remove API-key plan heuristics (unused)
This commit is contained in:
@@ -111,14 +111,29 @@ const ILLEGAL_ENV_VAR_PREFIX: &str = "CODEX_";
|
||||
/// Security: Do not allow `.env` files to create or modify any variables
|
||||
/// with names starting with `CODEX_`.
|
||||
fn load_dotenv() {
|
||||
// 1) Load from global ~/.code/.env (or ~/.codex/.env) first.
|
||||
if let Ok(codex_home) = codex_core::config::find_codex_home() {
|
||||
if let Ok(iter) = dotenvy::from_path_iter(codex_home.join(".env")) {
|
||||
// Global env may legitimately contain provider keys for Code usage.
|
||||
set_filtered(iter);
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Load from the current project's .env, but with extra safety:
|
||||
// - Do NOT import provider API keys by default (e.g., OPENAI_API_KEY, AZURE_OPENAI_API_KEY).
|
||||
// - Users can opt back in via CODEX_ALLOW_PROJECT_OPENAI_KEYS=1 (either exported
|
||||
// in the shell or placed in ~/.code/.env).
|
||||
if let Ok(iter) = dotenvy::dotenv_iter() {
|
||||
set_filtered(iter);
|
||||
// Filtered setter that always blocks provider keys from the project's .env.
|
||||
for (key, value) in iter.into_iter().flatten() {
|
||||
let upper = key.to_ascii_uppercase();
|
||||
// Never allow CODEX_* to be set from .env files for safety.
|
||||
if upper.starts_with(ILLEGAL_ENV_VAR_PREFIX) { continue; }
|
||||
// Always ignore provider keys from project .env (must be set globally or in shell).
|
||||
if upper == "OPENAI_API_KEY" || upper == "AZURE_OPENAI_API_KEY" { continue; }
|
||||
// Safe: still single-threaded during startup.
|
||||
unsafe { std::env::set_var(&key, &value) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -262,19 +262,20 @@ fn load_auth(
|
||||
last_refresh,
|
||||
} = auth_dot_json;
|
||||
|
||||
// If the auth.json has an API key AND does not appear to be on a plan that
|
||||
// should prefer AuthMode::ChatGPT, use AuthMode::ApiKey.
|
||||
// If the auth.json has an API key, decide whether to use it.
|
||||
if let Some(api_key) = &auth_json_api_key {
|
||||
// Should any of these be AuthMode::ChatGPT with the api_key set?
|
||||
// Does AuthMode::ChatGPT indicate that there is an auth.json that is
|
||||
// "refreshable" even if we are using the API key for auth?
|
||||
match &tokens {
|
||||
Some(tokens) => {
|
||||
if tokens.should_use_api_key(preferred_auth_method, tokens.is_openai_email()) {
|
||||
Some(_tokens) => {
|
||||
// When tokens are present, honor the caller's preference strictly:
|
||||
// - If the caller prefers API key, use it.
|
||||
// - Otherwise, prefer ChatGPT and ignore the API key.
|
||||
if preferred_auth_method == AuthMode::ApiKey {
|
||||
return Ok(Some(CodexAuth::from_api_key_with_client(api_key, client)));
|
||||
} else {
|
||||
// Ignore the API key and fall through to ChatGPT auth.
|
||||
}
|
||||
// else: fall through to ChatGPT auth
|
||||
}
|
||||
None => {
|
||||
// We have an API key but no tokens in the auth.json file.
|
||||
|
||||
@@ -1232,7 +1232,8 @@ impl Config {
|
||||
use codex_protocol::mcp_protocol::AuthMode;
|
||||
use crate::CodexAuth;
|
||||
|
||||
match CodexAuth::from_codex_home(codex_home, AuthMode::ApiKey, "codex_cli_rs") {
|
||||
// Prefer ChatGPT when both ChatGPT tokens and an API key are present.
|
||||
match CodexAuth::from_codex_home(codex_home, AuthMode::ChatGPT, "codex_cli_rs") {
|
||||
Ok(Some(auth)) => auth.mode == AuthMode::ChatGPT,
|
||||
_ => false,
|
||||
}
|
||||
|
||||
@@ -67,10 +67,10 @@ impl ConversationManager {
|
||||
}
|
||||
|
||||
pub async fn new_conversation(&self, config: Config) -> CodexResult<NewConversation> {
|
||||
// Build auth from codex_home using API key mode by default.
|
||||
// Build auth from codex_home preferring ChatGPT by default.
|
||||
let auth = CodexAuth::from_codex_home(
|
||||
&config.codex_home,
|
||||
AuthMode::ApiKey,
|
||||
AuthMode::ChatGPT,
|
||||
&config.responses_originator_header,
|
||||
)?;
|
||||
self.spawn_conversation(config, auth).await
|
||||
|
||||
@@ -3,7 +3,6 @@ use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use thiserror::Error;
|
||||
|
||||
use codex_protocol::mcp_protocol::AuthMode;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Default)]
|
||||
pub struct TokenData {
|
||||
@@ -23,27 +22,6 @@ pub struct TokenData {
|
||||
}
|
||||
|
||||
impl TokenData {
|
||||
/// Returns true if this is a plan that should use the traditional
|
||||
/// "metered" billing via an API key.
|
||||
pub(crate) fn should_use_api_key(
|
||||
&self,
|
||||
preferred_auth_method: AuthMode,
|
||||
is_openai_email: bool,
|
||||
) -> bool {
|
||||
if preferred_auth_method == AuthMode::ApiKey {
|
||||
return true;
|
||||
}
|
||||
// If the email is an OpenAI email, use AuthMode::ChatGPT unless preferred_auth_method is AuthMode::ApiKey.
|
||||
if is_openai_email {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.id_token
|
||||
.chatgpt_plan_type
|
||||
.as_ref()
|
||||
.is_none_or(|plan| plan.is_plan_that_should_use_api_key())
|
||||
}
|
||||
|
||||
pub fn is_openai_email(&self) -> bool {
|
||||
self.id_token
|
||||
.email
|
||||
@@ -80,19 +58,6 @@ pub(crate) enum PlanType {
|
||||
}
|
||||
|
||||
impl PlanType {
|
||||
fn is_plan_that_should_use_api_key(&self) -> bool {
|
||||
match self {
|
||||
Self::Known(known) => {
|
||||
use KnownPlan::*;
|
||||
!matches!(known, Free | Plus | Pro | Team)
|
||||
}
|
||||
Self::Unknown(_) => {
|
||||
// Unknown plans should use the API key.
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_string(&self) -> String {
|
||||
match self {
|
||||
Self::Known(known) => format!("{known:?}").to_lowercase(),
|
||||
|
||||
@@ -1501,6 +1501,15 @@ impl WidgetRef for ChatComposer {
|
||||
}
|
||||
}
|
||||
|
||||
// Show current auth mode. Make API key usage obvious.
|
||||
if !self.using_chatgpt_auth {
|
||||
if !left_spans.is_empty() { left_spans.push(Span::from(" ")); }
|
||||
left_spans.push(
|
||||
Span::from("Auth: API key")
|
||||
.style(Style::default().fg(crate::colors::warning()).add_modifier(Modifier::BOLD)),
|
||||
);
|
||||
}
|
||||
|
||||
// Right side: command key hints (Ctrl+R/D/H) followed by token usage if available
|
||||
// We will elide hints when space is tight in this order: hide reasoning, diff viewer.
|
||||
let mut right_spans: Vec<Span> = Vec::new();
|
||||
|
||||
@@ -441,7 +441,7 @@ pub enum LoginStatus {
|
||||
/// Determine current login status based on auth.json presence.
|
||||
pub fn get_login_status(config: &Config) -> LoginStatus {
|
||||
let codex_home = config.codex_home.clone();
|
||||
match CodexAuth::from_codex_home(&codex_home, AuthMode::ApiKey, &config.responses_originator_header) {
|
||||
match CodexAuth::from_codex_home(&codex_home, AuthMode::ChatGPT, &config.responses_originator_header) {
|
||||
Ok(Some(auth)) => LoginStatus::AuthMode(auth.mode),
|
||||
_ => LoginStatus::NotAuthenticated,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_core::config::set_project_trusted;
|
||||
use codex_core::config::set_project_access_mode;
|
||||
use codex_protocol::config_types::SandboxMode as SandboxModeCfg;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use crossterm::event::KeyCode;
|
||||
@@ -166,10 +168,21 @@ impl TrustDirectoryWidget {
|
||||
}
|
||||
|
||||
// Update the in-memory chat config for this session to a more permissive
|
||||
// policy suitable for a trusted workspace.
|
||||
// policy suitable for a trusted workspace (Full Access).
|
||||
if let Ok(mut args) = self.chat_widget_args.lock() {
|
||||
args.config.approval_policy = AskForApproval::OnRequest;
|
||||
args.config.sandbox_policy = SandboxPolicy::new_workspace_write_policy();
|
||||
args.config.approval_policy = AskForApproval::Never;
|
||||
args.config.sandbox_policy = SandboxPolicy::DangerFullAccess;
|
||||
}
|
||||
|
||||
// Persist the access mode explicitly so subsequent runs don't rely solely on
|
||||
// the trust fallback logic and so the UI reflects the chosen mode immediately.
|
||||
if let Err(e) = set_project_access_mode(
|
||||
&self.codex_home,
|
||||
&self.cwd,
|
||||
AskForApproval::Never,
|
||||
SandboxModeCfg::DangerFullAccess,
|
||||
) {
|
||||
tracing::warn!("Failed to persist project access mode: {e:?}");
|
||||
}
|
||||
|
||||
self.selection = Some(TrustDirectorySelection::Trust);
|
||||
|
||||
@@ -48,6 +48,23 @@ Notes:
|
||||
- When `preferred_auth_method = "chatgpt"` (default), Codex prefers ChatGPT auth if present; if only an API key is present, it will use the API key. Certain account types may also require API-key mode.
|
||||
- To check which auth method is being used during a session, use the `/status` command in the TUI.
|
||||
|
||||
## Project .env safety (OPENAI_API_KEY)
|
||||
|
||||
By default, Codex will no longer read `OPENAI_API_KEY` or `AZURE_OPENAI_API_KEY` from a project’s local `.env` file.
|
||||
|
||||
Why: many repos include an API key in `.env` for unrelated tooling, which could cause Codex to silently use the API key instead of your ChatGPT plan in that folder.
|
||||
|
||||
What still works:
|
||||
|
||||
- `~/.code/.env` (or `~/.codex/.env`) is loaded first and may contain your `OPENAI_API_KEY` for global use.
|
||||
- A shell-exported `OPENAI_API_KEY` is honored.
|
||||
|
||||
Project `.env` provider keys are always ignored — there is no opt‑in.
|
||||
|
||||
UI clarity:
|
||||
|
||||
- When Codex is using an API key, the chat footer shows a bold “Auth: API key” badge so it’s obvious which mode you’re in.
|
||||
|
||||
## Connecting on a "Headless" Machine
|
||||
|
||||
Today, the login process entails running a server on `localhost:1455`. If you are on a "headless" server, such as a Docker container or are `ssh`'d into a remote machine, loading `localhost:1455` in the browser on your local machine will not automatically connect to the webserver running on the _headless_ machine, so you must use one of the following workarounds:
|
||||
|
||||
Reference in New Issue
Block a user