Compare commits

..

2 Commits

Author SHA1 Message Date
Michael Bolin
40ba3162cc core: route view_image through a sandbox-backed fs helper 2026-03-19 09:18:57 -07:00
Michael Bolin
392347d436 fix: try to fix "Stage npm package" step in ci.yml (#15092)
Fix the CI job by updating it to use artifacts from a more recent
release (`0.115.0`) instead of the existing one (`0.74.0`).

This step in our CI job on PRs started failing today:


334164a6f7/.github/workflows/ci.yml (L33-L47)

I believe it's because this test verifies that the "package npm" script
works, but we want it to be fast and not wait for binaries to be built,
so it uses a GitHub workflow that's already done. Because it was using a
GitHub workflow associated with `0.74.0`, it seems likely that
workflow's history has been reaped, so we need to use a newer one.
2026-03-18 13:52:33 -07:00
48 changed files with 980 additions and 605 deletions

View File

@@ -37,7 +37,7 @@ jobs:
run: |
set -euo pipefail
# Use a rust-release version that includes all native binaries.
CODEX_VERSION=0.74.0
CODEX_VERSION=0.115.0
OUTPUT_DIR="${RUNNER_TEMP}"
python3 ./scripts/stage_npm_packages.py \
--release-version "$CODEX_VERSION" \

55
codex-rs/Cargo.lock generated
View File

@@ -1557,6 +1557,7 @@ version = "0.0.0"
dependencies = [
"anyhow",
"codex-apply-patch",
"codex-fs-ops",
"codex-linux-sandbox",
"codex-shell-escalation",
"codex-utils-home-dir",
@@ -1596,23 +1597,6 @@ dependencies = [
"tokio-util",
]
[[package]]
name = "codex-auth"
version = "0.0.0"
dependencies = [
"base64 0.22.1",
"codex-api",
"codex-app-server-protocol",
"http 1.4.0",
"maplit",
"pretty_assertions",
"schemars 0.8.22",
"serde",
"serde_json",
"thiserror 2.0.18",
"toml 0.9.11+spec-1.1.0",
]
[[package]]
name = "codex-backend-client"
version = "0.0.0"
@@ -1857,14 +1841,13 @@ dependencies = [
"codex-arg0",
"codex-artifacts",
"codex-async-utils",
"codex-auth",
"codex-client",
"codex-config",
"codex-connectors",
"codex-core-auth",
"codex-environment",
"codex-execpolicy",
"codex-file-search",
"codex-fs-ops",
"codex-git",
"codex-hooks",
"codex-keyring-store",
@@ -1954,28 +1937,6 @@ dependencies = [
"zstd",
]
[[package]]
name = "codex-core-auth"
version = "0.0.0"
dependencies = [
"anyhow",
"base64 0.22.1",
"chrono",
"codex-app-server-protocol",
"codex-auth",
"codex-keyring-store",
"keyring",
"once_cell",
"pretty_assertions",
"schemars 0.8.22",
"serde",
"serde_json",
"sha2",
"tempfile",
"tokio",
"tracing",
]
[[package]]
name = "codex-debug-client"
version = "0.0.0"
@@ -2118,6 +2079,18 @@ dependencies = [
"tokio",
]
[[package]]
name = "codex-fs-ops"
version = "0.0.0"
dependencies = [
"anyhow",
"base64 0.22.1",
"pretty_assertions",
"serde",
"serde_json",
"tempfile",
]
[[package]]
name = "codex-git"
version = "0.0.0"

View File

@@ -11,6 +11,7 @@ members = [
"apply-patch",
"arg0",
"feedback",
"fs-ops",
"codex-backend-openapi-models",
"cloud-requirements",
"cloud-tasks",
@@ -18,12 +19,10 @@ members = [
"cli",
"connectors",
"config",
"codex-auth",
"shell-command",
"shell-escalation",
"skills",
"core",
"core/auth",
"environment",
"hooks",
"secrets",
@@ -89,7 +88,6 @@ license = "Apache-2.0"
app_test_support = { path = "app-server/tests/common" }
codex-ansi-escape = { path = "ansi-escape" }
codex-api = { path = "codex-api" }
codex-auth = { path = "codex-auth" }
codex-artifacts = { path = "artifacts" }
codex-package-manager = { path = "package-manager" }
codex-app-server = { path = "app-server" }
@@ -107,12 +105,12 @@ codex-cloud-requirements = { path = "cloud-requirements" }
codex-connectors = { path = "connectors" }
codex-config = { path = "config" }
codex-core = { path = "core" }
codex-core-auth = { path = "core/auth" }
codex-environment = { path = "environment" }
codex-exec = { path = "exec" }
codex-execpolicy = { path = "execpolicy" }
codex-experimental-api-macros = { path = "codex-experimental-api-macros" }
codex-feedback = { path = "feedback" }
codex-fs-ops = { path = "fs-ops" }
codex-file-search = { path = "file-search" }
codex-git = { path = "utils/git" }
codex-hooks = { path = "hooks" }

View File

@@ -7,11 +7,11 @@ use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use chrono::DateTime;
use chrono::Utc;
use codex_app_server_protocol::AuthMode;
use codex_core::TokenData;
use codex_core::auth::AuthCredentialsStoreMode;
use codex_core::auth::AuthDotJson;
use codex_core::auth::save_auth;
use codex_core::parse_chatgpt_jwt_claims;
use codex_core::token_data::TokenData;
use codex_core::token_data::parse_chatgpt_jwt_claims;
use serde_json::json;
/// Builder for writing a fake ChatGPT auth.json in tests.

View File

@@ -14,6 +14,7 @@ workspace = true
[dependencies]
anyhow = { workspace = true }
codex-apply-patch = { workspace = true }
codex-fs-ops = { workspace = true }
codex-linux-sandbox = { workspace = true }
codex-shell-escalation = { workspace = true }
codex-utils-home-dir = { workspace = true }

View File

@@ -4,6 +4,7 @@ use std::path::Path;
use std::path::PathBuf;
use codex_apply_patch::CODEX_CORE_APPLY_PATCH_ARG1;
use codex_fs_ops::CODEX_CORE_FS_OPS_ARG1;
use codex_utils_home_dir::find_codex_home;
#[cfg(unix)]
use std::os::unix::fs::symlink;
@@ -105,6 +106,17 @@ pub fn arg0_dispatch() -> Option<Arg0PathEntryGuard> {
};
std::process::exit(exit_code);
}
if argv1 == CODEX_CORE_FS_OPS_ARG1 {
let mut stdin = std::io::stdin();
let mut stdout = std::io::stdout();
let mut stderr = std::io::stderr();
let exit_code =
match codex_fs_ops::run_from_args(args, &mut stdin, &mut stdout, &mut stderr) {
Ok(()) => 0,
Err(_) => 1,
};
std::process::exit(exit_code);
}
// This modifies the environment, which is not thread-safe, so do this
// before creating any threads/the Tokio runtime.

View File

@@ -1,10 +1,10 @@
use codex_core::AuthManager;
use codex_core::TokenData;
use std::path::Path;
use std::sync::LazyLock;
use std::sync::RwLock;
use codex_core::auth::AuthCredentialsStoreMode;
use codex_core::token_data::TokenData;
static CHATGPT_TOKEN: LazyLock<RwLock<Option<TokenData>>> = LazyLock::new(|| RwLock::new(None));

View File

@@ -1,6 +1,6 @@
use codex_core::AuthManager;
use codex_core::TokenData;
use codex_core::config::Config;
use codex_core::token_data::TokenData;
use std::collections::HashSet;
use std::time::Duration;

View File

@@ -1,15 +0,0 @@
#[derive(Debug)]
pub struct EnvVarError {
pub var: String,
pub instructions: Option<String>,
}
impl std::fmt::Display for EnvVarError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Missing environment variable: `{}`.", self.var)?;
if let Some(instructions) = &self.instructions {
write!(f, " {instructions}")?;
}
Ok(())
}
}

View File

@@ -1,19 +0,0 @@
pub mod error;
pub mod provider;
pub mod token_data;
#[cfg(test)]
mod model_provider_info_tests;
#[cfg(test)]
mod token_data_tests;
pub use error::EnvVarError;
pub use provider::DEFAULT_LMSTUDIO_PORT;
pub use provider::DEFAULT_OLLAMA_PORT;
pub use provider::LMSTUDIO_OSS_PROVIDER_ID;
pub use provider::ModelProviderInfo;
pub use provider::OLLAMA_OSS_PROVIDER_ID;
pub use provider::OPENAI_PROVIDER_ID;
pub use provider::WireApi;
pub use provider::built_in_model_providers;
pub use provider::create_oss_provider_with_base_url;

View File

@@ -1,291 +0,0 @@
use crate::error::EnvVarError;
use codex_api::Provider as ApiProvider;
use codex_api::provider::RetryConfig as ApiRetryConfig;
use codex_app_server_protocol::AuthMode as ApiAuthMode;
use http::HeaderMap;
use http::header::HeaderName;
use http::header::HeaderValue;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::fmt;
use std::time::Duration;
const DEFAULT_STREAM_IDLE_TIMEOUT_MS: u64 = 300_000;
const DEFAULT_STREAM_MAX_RETRIES: u64 = 5;
const DEFAULT_REQUEST_MAX_RETRIES: u64 = 4;
pub const DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS: u64 = 15_000;
const MAX_STREAM_MAX_RETRIES: u64 = 100;
const MAX_REQUEST_MAX_RETRIES: u64 = 100;
const OPENAI_PROVIDER_NAME: &str = "OpenAI";
pub const OPENAI_PROVIDER_ID: &str = "openai";
pub const CHAT_WIRE_API_REMOVED_ERROR: &str = "`wire_api = \"chat\"` is no longer supported.\nHow to fix: set `wire_api = \"responses\"` in your provider config.\nMore info: https://github.com/openai/codex/discussions/7782";
pub const LEGACY_OLLAMA_CHAT_PROVIDER_ID: &str = "ollama-chat";
pub const OLLAMA_CHAT_PROVIDER_REMOVED_ERROR: &str = "`ollama-chat` is no longer supported.\nHow to fix: replace `ollama-chat` with `ollama` in `model_provider`, `oss_provider`, or `--local-provider`.\nMore info: https://github.com/openai/codex/discussions/7782";
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
#[schemars(rename = "WireApi")]
pub enum WireApi {
#[default]
Responses,
}
impl fmt::Display for WireApi {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let value = match self {
Self::Responses => "responses",
};
f.write_str(value)
}
}
impl<'de> Deserialize<'de> for WireApi {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
match value.as_str() {
"responses" => Ok(Self::Responses),
"chat" => Err(serde::de::Error::custom(CHAT_WIRE_API_REMOVED_ERROR)),
_ => Err(serde::de::Error::unknown_variant(&value, &["responses"])),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema)]
#[schemars(deny_unknown_fields)]
#[schemars(rename = "ModelProviderInfo")]
pub struct ModelProviderInfo {
pub name: String,
pub base_url: Option<String>,
pub env_key: Option<String>,
pub env_key_instructions: Option<String>,
pub experimental_bearer_token: Option<String>,
#[serde(default)]
pub wire_api: WireApi,
pub query_params: Option<HashMap<String, String>>,
pub http_headers: Option<HashMap<String, String>>,
pub env_http_headers: Option<HashMap<String, String>>,
pub request_max_retries: Option<u64>,
pub stream_max_retries: Option<u64>,
pub stream_idle_timeout_ms: Option<u64>,
#[schemars(
description = "Maximum time (in milliseconds) to wait for a websocket connection attempt before treating it as failed."
)]
pub websocket_connect_timeout_ms: Option<u64>,
#[serde(default)]
pub requires_openai_auth: bool,
#[serde(default)]
pub supports_websockets: bool,
}
impl ModelProviderInfo {
fn build_header_map(&self) -> HeaderMap {
let capacity = self.http_headers.as_ref().map_or(0, HashMap::len)
+ self.env_http_headers.as_ref().map_or(0, HashMap::len);
let mut headers = HeaderMap::with_capacity(capacity);
if let Some(extra) = &self.http_headers {
for (k, v) in extra {
if let (Ok(name), Ok(value)) = (HeaderName::try_from(k), HeaderValue::try_from(v)) {
headers.insert(name, value);
}
}
}
if let Some(env_headers) = &self.env_http_headers {
for (header, env_var) in env_headers {
if let Ok(val) = std::env::var(env_var)
&& !val.trim().is_empty()
&& let (Ok(name), Ok(value)) =
(HeaderName::try_from(header), HeaderValue::try_from(val))
{
headers.insert(name, value);
}
}
}
headers
}
pub fn to_api_provider(
&self,
auth_mode: Option<ApiAuthMode>,
) -> Result<ApiProvider, EnvVarError> {
let default_base_url = if matches!(
auth_mode,
Some(ApiAuthMode::Chatgpt | ApiAuthMode::ChatgptAuthTokens)
) {
"https://chatgpt.com/backend-api/codex"
} else {
"https://api.openai.com/v1"
};
let base_url = self
.base_url
.clone()
.unwrap_or_else(|| default_base_url.to_string());
let retry = ApiRetryConfig {
max_attempts: self.request_max_retries(),
base_delay: Duration::from_millis(200),
retry_429: false,
retry_5xx: true,
retry_transport: true,
};
Ok(ApiProvider {
name: self.name.clone(),
base_url,
query_params: self.query_params.clone(),
headers: self.build_header_map(),
retry,
stream_idle_timeout: self.stream_idle_timeout(),
})
}
pub fn api_key(&self) -> Result<Option<String>, EnvVarError> {
match &self.env_key {
Some(env_key) => {
let api_key = std::env::var(env_key)
.ok()
.filter(|v| !v.trim().is_empty())
.ok_or_else(|| EnvVarError {
var: env_key.clone(),
instructions: self.env_key_instructions.clone(),
})?;
Ok(Some(api_key))
}
None => Ok(None),
}
}
pub fn request_max_retries(&self) -> u64 {
self.request_max_retries
.unwrap_or(DEFAULT_REQUEST_MAX_RETRIES)
.min(MAX_REQUEST_MAX_RETRIES)
}
pub fn stream_max_retries(&self) -> u64 {
self.stream_max_retries
.unwrap_or(DEFAULT_STREAM_MAX_RETRIES)
.min(MAX_STREAM_MAX_RETRIES)
}
pub fn stream_idle_timeout(&self) -> Duration {
self.stream_idle_timeout_ms
.map(Duration::from_millis)
.unwrap_or(Duration::from_millis(DEFAULT_STREAM_IDLE_TIMEOUT_MS))
}
pub fn websocket_connect_timeout(&self) -> Duration {
self.websocket_connect_timeout_ms
.map(Duration::from_millis)
.unwrap_or(Duration::from_millis(DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS))
}
pub fn create_openai_provider(base_url: Option<String>) -> ModelProviderInfo {
ModelProviderInfo {
name: OPENAI_PROVIDER_NAME.into(),
base_url,
env_key: None,
env_key_instructions: None,
experimental_bearer_token: None,
wire_api: WireApi::Responses,
query_params: None,
http_headers: Some(
[("version".to_string(), env!("CARGO_PKG_VERSION").to_string())]
.into_iter()
.collect(),
),
env_http_headers: Some(
[
(
"OpenAI-Organization".to_string(),
"OPENAI_ORGANIZATION".to_string(),
),
("OpenAI-Project".to_string(), "OPENAI_PROJECT".to_string()),
]
.into_iter()
.collect(),
),
request_max_retries: None,
stream_max_retries: None,
stream_idle_timeout_ms: None,
websocket_connect_timeout_ms: None,
requires_openai_auth: true,
supports_websockets: true,
}
}
pub fn is_openai(&self) -> bool {
self.name == OPENAI_PROVIDER_NAME
}
}
pub const DEFAULT_LMSTUDIO_PORT: u16 = 1234;
pub const DEFAULT_OLLAMA_PORT: u16 = 11434;
pub const LMSTUDIO_OSS_PROVIDER_ID: &str = "lmstudio";
pub const OLLAMA_OSS_PROVIDER_ID: &str = "ollama";
pub fn built_in_model_providers(
openai_base_url: Option<String>,
) -> HashMap<String, ModelProviderInfo> {
use ModelProviderInfo as P;
let openai_provider = P::create_openai_provider(openai_base_url);
[
(OPENAI_PROVIDER_ID, openai_provider),
(
OLLAMA_OSS_PROVIDER_ID,
create_oss_provider(DEFAULT_OLLAMA_PORT, WireApi::Responses),
),
(
LMSTUDIO_OSS_PROVIDER_ID,
create_oss_provider(DEFAULT_LMSTUDIO_PORT, WireApi::Responses),
),
]
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect()
}
pub fn create_oss_provider(default_provider_port: u16, wire_api: WireApi) -> ModelProviderInfo {
let default_codex_oss_base_url = format!(
"http://localhost:{codex_oss_port}/v1",
codex_oss_port = std::env::var("CODEX_OSS_PORT")
.ok()
.filter(|value| !value.trim().is_empty())
.and_then(|value| value.parse::<u16>().ok())
.unwrap_or(default_provider_port)
);
let codex_oss_base_url = std::env::var("CODEX_OSS_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty())
.unwrap_or(default_codex_oss_base_url);
create_oss_provider_with_base_url(&codex_oss_base_url, wire_api)
}
pub fn create_oss_provider_with_base_url(base_url: &str, wire_api: WireApi) -> ModelProviderInfo {
ModelProviderInfo {
name: "gpt-oss".into(),
base_url: Some(base_url.into()),
env_key: None,
env_key_instructions: None,
experimental_bearer_token: None,
wire_api,
query_params: None,
http_headers: None,
env_http_headers: None,
request_max_retries: None,
stream_max_retries: None,
stream_idle_timeout_ms: None,
websocket_connect_timeout_ms: None,
requires_openai_auth: false,
supports_websockets: false,
}
}

View File

@@ -28,18 +28,17 @@ chardetng = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
clap = { workspace = true, features = ["derive"] }
codex-api = { workspace = true }
codex-auth = { workspace = true }
codex-app-server-protocol = { workspace = true }
codex-apply-patch = { workspace = true }
codex-async-utils = { workspace = true }
codex-client = { workspace = true }
codex-connectors = { workspace = true }
codex-config = { workspace = true }
codex-core-auth = { workspace = true }
codex-environment = { workspace = true }
codex-shell-command = { workspace = true }
codex-skills = { workspace = true }
codex-execpolicy = { workspace = true }
codex-fs-ops = { workspace = true }
codex-file-search = { workspace = true }
codex-git = { workspace = true }
codex-hooks = { workspace = true }

View File

@@ -1,29 +0,0 @@
[package]
name = "codex-core-auth"
version.workspace = true
edition.workspace = true
license.workspace = true
[lints]
workspace = true
[dependencies]
chrono = { workspace = true, features = ["serde"] }
codex-app-server-protocol = { workspace = true }
codex-auth = { workspace = true }
codex-keyring-store = { workspace = true }
once_cell = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
sha2 = { workspace = true }
tracing = { workspace = true }
[dev-dependencies]
anyhow = { workspace = true }
base64 = { workspace = true }
keyring = { workspace = true }
pretty_assertions = { workspace = true }
serde = { workspace = true, features = ["derive"] }
tempfile = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }

View File

@@ -1,14 +0,0 @@
//! Auth storage backend for Codex CLI credentials.
//!
//! This crate provides the storage layer for auth.json (file, keyring, auto, ephemeral)
//! and the AuthDotJson / AuthCredentialsStoreMode types. The higher-level auth logic
//! (CodexAuth, AuthManager, token refresh) lives in codex-core.
pub mod storage;
pub use storage::AuthCredentialsStoreMode;
pub use storage::AuthDotJson;
pub use storage::AuthStorageBackend;
pub use storage::FileAuthStorage;
pub use storage::create_auth_storage;
pub use storage::get_auth_file;

View File

@@ -10,13 +10,13 @@ use http::HeaderMap;
use serde::Deserialize;
use serde_json::Value;
use crate::PlanType;
use crate::auth::CodexAuth;
use crate::error::CodexErr;
use crate::error::RetryLimitReachedError;
use crate::error::UnexpectedResponseError;
use crate::error::UsageLimitReachedError;
use crate::model_provider_info::ModelProviderInfo;
use crate::token_data::PlanType;
pub(crate) fn map_api_error(err: ApiError) -> CodexErr {
match err {

View File

@@ -1,3 +1,5 @@
mod storage;
use async_trait::async_trait;
use chrono::Utc;
use reqwest::StatusCode;
@@ -17,19 +19,17 @@ use codex_app_server_protocol::AuthMode as ApiAuthMode;
use codex_otel::TelemetryAuthMode;
use codex_protocol::config_types::ForcedLoginMethod;
use codex_core_auth::AuthStorageBackend;
use codex_core_auth::create_auth_storage;
pub use codex_core_auth::AuthCredentialsStoreMode;
pub use codex_core_auth::AuthDotJson;
use crate::KnownPlan as InternalKnownPlan;
use crate::PlanType as InternalPlanType;
use crate::TokenData;
pub use crate::auth::storage::AuthCredentialsStoreMode;
pub use crate::auth::storage::AuthDotJson;
use crate::auth::storage::AuthStorageBackend;
use crate::auth::storage::create_auth_storage;
use crate::config::Config;
use crate::error::RefreshTokenFailedError;
use crate::error::RefreshTokenFailedReason;
use crate::parse_chatgpt_jwt_claims;
use crate::token_data::KnownPlan as InternalKnownPlan;
use crate::token_data::PlanType as InternalPlanType;
use crate::token_data::TokenData;
use crate::token_data::parse_chatgpt_jwt_claims;
use crate::util::try_parse_error_message;
use codex_client::CodexHttpClient;
use codex_protocol::account::PlanType as AccountPlanType;
@@ -752,6 +752,67 @@ fn refresh_token_endpoint() -> String {
.unwrap_or_else(|_| REFRESH_TOKEN_URL.to_string())
}
impl AuthDotJson {
fn from_external_tokens(external: &ExternalAuthTokens) -> std::io::Result<Self> {
let mut token_info =
parse_chatgpt_jwt_claims(&external.access_token).map_err(std::io::Error::other)?;
token_info.chatgpt_account_id = Some(external.chatgpt_account_id.clone());
token_info.chatgpt_plan_type = external
.chatgpt_plan_type
.as_deref()
.map(InternalPlanType::from_raw_value)
.or(token_info.chatgpt_plan_type)
.or(Some(InternalPlanType::Unknown("unknown".to_string())));
let tokens = TokenData {
id_token: token_info,
access_token: external.access_token.clone(),
refresh_token: String::new(),
account_id: Some(external.chatgpt_account_id.clone()),
};
Ok(Self {
auth_mode: Some(ApiAuthMode::ChatgptAuthTokens),
openai_api_key: None,
tokens: Some(tokens),
last_refresh: Some(Utc::now()),
})
}
fn from_external_access_token(
access_token: &str,
chatgpt_account_id: &str,
chatgpt_plan_type: Option<&str>,
) -> std::io::Result<Self> {
let external = ExternalAuthTokens {
access_token: access_token.to_string(),
chatgpt_account_id: chatgpt_account_id.to_string(),
chatgpt_plan_type: chatgpt_plan_type.map(str::to_string),
};
Self::from_external_tokens(&external)
}
fn resolved_mode(&self) -> ApiAuthMode {
if let Some(mode) = self.auth_mode {
return mode;
}
if self.openai_api_key.is_some() {
return ApiAuthMode::ApiKey;
}
ApiAuthMode::Chatgpt
}
fn storage_mode(
&self,
auth_credentials_store_mode: AuthCredentialsStoreMode,
) -> AuthCredentialsStoreMode {
if self.resolved_mode() == ApiAuthMode::ChatgptAuthTokens {
AuthCredentialsStoreMode::Ephemeral
} else {
auth_credentials_store_mode
}
}
}
/// Internal cached auth state.
#[derive(Clone)]
struct CachedAuth {
@@ -1351,12 +1412,8 @@ impl AuthManager {
),
)));
}
let auth_dot_json = AuthDotJson::from_external_access_token(
&refreshed.access_token,
&refreshed.chatgpt_account_id,
refreshed.chatgpt_plan_type.as_deref(),
)
.map_err(RefreshTokenError::Transient)?;
let auth_dot_json =
AuthDotJson::from_external_tokens(&refreshed).map_err(RefreshTokenError::Transient)?;
save_auth(
&self.codex_home,
&auth_dot_json,

View File

@@ -19,10 +19,8 @@ use std::sync::Arc;
use std::sync::Mutex;
use tracing::warn;
use crate::token_data::TokenData;
use codex_app_server_protocol::AuthMode;
use codex_auth::token_data::PlanType;
use codex_auth::token_data::TokenData;
use codex_auth::token_data::parse_chatgpt_jwt_claims;
use codex_keyring_store::DefaultKeyringStore;
use codex_keyring_store::KeyringStore;
use once_cell::sync::Lazy;
@@ -58,57 +56,7 @@ pub struct AuthDotJson {
pub last_refresh: Option<DateTime<Utc>>,
}
impl AuthDotJson {
pub fn from_external_access_token(
access_token: &str,
chatgpt_account_id: &str,
chatgpt_plan_type: Option<&str>,
) -> std::io::Result<Self> {
let mut token_info =
parse_chatgpt_jwt_claims(access_token).map_err(std::io::Error::other)?;
token_info.chatgpt_account_id = Some(chatgpt_account_id.to_string());
token_info.chatgpt_plan_type = chatgpt_plan_type
.map(PlanType::from_raw_value)
.or(token_info.chatgpt_plan_type)
.or(Some(PlanType::Unknown("unknown".to_string())));
let tokens = TokenData {
id_token: token_info,
access_token: access_token.to_string(),
refresh_token: String::new(),
account_id: Some(chatgpt_account_id.to_string()),
};
Ok(Self {
auth_mode: Some(AuthMode::ChatgptAuthTokens),
openai_api_key: None,
tokens: Some(tokens),
last_refresh: Some(Utc::now()),
})
}
pub fn resolved_mode(&self) -> AuthMode {
if let Some(mode) = self.auth_mode {
return mode;
}
if self.openai_api_key.is_some() {
return AuthMode::ApiKey;
}
AuthMode::Chatgpt
}
pub fn storage_mode(
&self,
auth_credentials_store_mode: AuthCredentialsStoreMode,
) -> AuthCredentialsStoreMode {
if self.resolved_mode() == AuthMode::ChatgptAuthTokens {
AuthCredentialsStoreMode::Ephemeral
} else {
auth_credentials_store_mode
}
}
}
pub fn get_auth_file(codex_home: &Path) -> PathBuf {
pub(super) fn get_auth_file(codex_home: &Path) -> PathBuf {
codex_home.join("auth.json")
}
@@ -121,25 +69,25 @@ pub(super) fn delete_file_if_exists(codex_home: &Path) -> std::io::Result<bool>
}
}
pub trait AuthStorageBackend: Debug + Send + Sync {
pub(super) trait AuthStorageBackend: Debug + Send + Sync {
fn load(&self) -> std::io::Result<Option<AuthDotJson>>;
fn save(&self, auth: &AuthDotJson) -> std::io::Result<()>;
fn delete(&self) -> std::io::Result<bool>;
}
#[derive(Clone, Debug)]
pub struct FileAuthStorage {
pub(super) struct FileAuthStorage {
codex_home: PathBuf,
}
impl FileAuthStorage {
pub fn new(codex_home: PathBuf) -> Self {
pub(super) fn new(codex_home: PathBuf) -> Self {
Self { codex_home }
}
/// Attempt to read and parse the `auth.json` file in the given `CODEX_HOME` directory.
/// Returns the full AuthDotJson structure.
pub fn try_read_auth_json(&self, auth_file: &Path) -> std::io::Result<AuthDotJson> {
pub(super) fn try_read_auth_json(&self, auth_file: &Path) -> std::io::Result<AuthDotJson> {
let mut file = File::open(auth_file)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
@@ -187,7 +135,7 @@ impl AuthStorageBackend for FileAuthStorage {
const KEYRING_SERVICE: &str = "Codex Auth";
// turns codex_home path into a stable, short key string
pub(crate) fn compute_store_key(codex_home: &Path) -> std::io::Result<String> {
fn compute_store_key(codex_home: &Path) -> std::io::Result<String> {
let canonical = codex_home
.canonicalize()
.unwrap_or_else(|_| codex_home.to_path_buf());
@@ -201,13 +149,13 @@ pub(crate) fn compute_store_key(codex_home: &Path) -> std::io::Result<String> {
}
#[derive(Clone, Debug)]
pub(crate) struct KeyringAuthStorage {
pub(crate) codex_home: PathBuf,
pub(crate) keyring_store: Arc<dyn KeyringStore>,
struct KeyringAuthStorage {
codex_home: PathBuf,
keyring_store: Arc<dyn KeyringStore>,
}
impl KeyringAuthStorage {
pub(crate) fn new(codex_home: PathBuf, keyring_store: Arc<dyn KeyringStore>) -> Self {
fn new(codex_home: PathBuf, keyring_store: Arc<dyn KeyringStore>) -> Self {
Self {
codex_home,
keyring_store,
@@ -275,13 +223,13 @@ impl AuthStorageBackend for KeyringAuthStorage {
}
#[derive(Clone, Debug)]
pub(crate) struct AutoAuthStorage {
pub(crate) keyring_storage: Arc<KeyringAuthStorage>,
pub(crate) file_storage: Arc<FileAuthStorage>,
struct AutoAuthStorage {
keyring_storage: Arc<KeyringAuthStorage>,
file_storage: Arc<FileAuthStorage>,
}
impl AutoAuthStorage {
pub(crate) fn new(codex_home: PathBuf, keyring_store: Arc<dyn KeyringStore>) -> Self {
fn new(codex_home: PathBuf, keyring_store: Arc<dyn KeyringStore>) -> Self {
Self {
keyring_storage: Arc::new(KeyringAuthStorage::new(codex_home.clone(), keyring_store)),
file_storage: Arc::new(FileAuthStorage::new(codex_home)),
@@ -360,7 +308,7 @@ impl AuthStorageBackend for EphemeralAuthStorage {
}
}
pub fn create_auth_storage(
pub(super) fn create_auth_storage(
codex_home: PathBuf,
mode: AuthCredentialsStoreMode,
) -> Arc<dyn AuthStorageBackend> {

View File

@@ -1,16 +1,14 @@
use super::*;
use crate::token_data::IdTokenInfo;
use anyhow::Context;
use base64::Engine;
use codex_auth::token_data::IdTokenInfo;
use codex_auth::token_data::TokenData;
use codex_auth::token_data::parse_chatgpt_jwt_claims;
use codex_keyring_store::tests::MockKeyringStore;
use keyring::Error as KeyringError;
use pretty_assertions::assert_eq;
use serde::Serialize;
use serde_json::json;
use tempfile::tempdir;
use codex_keyring_store::tests::MockKeyringStore;
use keyring::Error as KeyringError;
#[tokio::test]
async fn file_storage_load_returns_auth_dot_json() -> anyhow::Result<()> {
let codex_home = tempdir()?;
@@ -169,7 +167,7 @@ fn id_token_with_prefix(prefix: &str) -> IdTokenInfo {
let signature_b64 = encode(b"sig");
let fake_jwt = format!("{header_b64}.{payload_b64}.{signature_b64}");
parse_chatgpt_jwt_claims(&fake_jwt).expect("fake JWT should parse")
crate::token_data::parse_chatgpt_jwt_claims(&fake_jwt).expect("fake JWT should parse")
}
fn auth_with_prefix(prefix: &str) -> AuthDotJson {

View File

@@ -1,12 +1,11 @@
use super::*;
use crate::IdTokenInfo;
use crate::KnownPlan as InternalKnownPlan;
use crate::PlanType as InternalPlanType;
use crate::TokenData;
use crate::auth::storage::FileAuthStorage;
use crate::auth::storage::get_auth_file;
use crate::config::Config;
use crate::config::ConfigBuilder;
use codex_core_auth::FileAuthStorage;
use codex_core_auth::get_auth_file;
use crate::token_data::IdTokenInfo;
use crate::token_data::KnownPlan as InternalKnownPlan;
use crate::token_data::PlanType as InternalPlanType;
use codex_protocol::account::PlanType as AccountPlanType;
use base64::Engine;

View File

@@ -528,7 +528,7 @@ impl ModelClient {
let api_provider = self
.state
.provider
.to_api_provider(auth.as_ref().map(CodexAuth::api_auth_mode))?;
.to_api_provider(auth.as_ref().map(CodexAuth::auth_mode))?;
let api_auth = auth_provider_from_auth(auth.clone(), &self.state.provider)?;
Ok(CurrentClientSetup {
auth,

View File

@@ -25,7 +25,6 @@ use tracing::warn;
use crate::AuthManager;
use crate::CodexAuth;
use crate::SandboxState;
use crate::TokenData;
use crate::config::Config;
use crate::config::types::AppToolApproval;
use crate::config::types::AppsConfigToml;
@@ -45,6 +44,7 @@ use crate::mcp_connection_manager::codex_apps_tools_cache_key;
use crate::plugins::AppConnectorId;
use crate::plugins::PluginsManager;
use crate::plugins::list_tool_suggest_discoverable_plugins;
use crate::token_data::TokenData;
use crate::tools::discoverable::DiscoverablePluginInfo;
use crate::tools::discoverable::DiscoverableTool;

View File

@@ -1,7 +1,7 @@
use crate::KnownPlan;
use crate::PlanType;
use crate::exec::ExecToolCallOutput;
use crate::network_policy_decision::NetworkPolicyDecisionPayload;
use crate::token_data::KnownPlan;
use crate::token_data::PlanType;
use crate::truncate::TruncationPolicy;
use crate::truncate::truncate_text;
use chrono::DateTime;
@@ -9,7 +9,6 @@ use chrono::Datelike;
use chrono::Local;
use chrono::Utc;
use codex_async_utils::CancelErr;
pub use codex_auth::EnvVarError;
use codex_protocol::ThreadId;
use codex_protocol::protocol::CodexErrorInfo;
use codex_protocol::protocol::ErrorEvent;
@@ -192,12 +191,6 @@ impl From<CancelErr> for CodexErr {
}
}
impl From<EnvVarError> for CodexErr {
fn from(error: EnvVarError) -> Self {
Self::EnvVar(error)
}
}
impl CodexErr {
pub fn is_retryable(&self) -> bool {
match self {
@@ -558,6 +551,26 @@ fn now_for_retry() -> DateTime<Utc> {
Utc::now()
}
#[derive(Debug)]
pub struct EnvVarError {
/// Name of the environment variable that is missing.
pub var: String,
/// Optional instructions to help the user get a valid value for the
/// variable and set it.
pub instructions: Option<String>,
}
impl std::fmt::Display for EnvVarError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Missing environment variable: `{}`.", self.var)?;
if let Some(instructions) = &self.instructions {
write!(f, " {instructions}")?;
}
Ok(())
}
}
impl CodexErr {
/// Minimal shim so that existing `e.downcast_ref::<CodexErr>()` checks continue to compile
/// after replacing `anyhow::Error` in the return signature. This mirrors the behavior of

View File

@@ -332,6 +332,58 @@ pub(crate) async fn execute_exec_request(
finalize_exec_result(raw_output_result, sandbox, duration)
}
pub(crate) async fn execute_exec_request_bytes(
exec_request: ExecRequest,
sandbox_policy: &SandboxPolicy,
stdout_stream: Option<StdoutStream>,
after_spawn: Option<Box<dyn FnOnce() + Send>>,
) -> Result<ExecToolCallOutputBytes> {
let ExecRequest {
command,
cwd,
env,
network,
expiration,
sandbox,
windows_sandbox_level,
windows_sandbox_private_desktop,
sandbox_permissions,
sandbox_policy: _sandbox_policy_from_env,
file_system_sandbox_policy,
network_sandbox_policy,
justification,
arg0,
} = exec_request;
let _ = _sandbox_policy_from_env;
let params = ExecParams {
command,
cwd,
expiration,
env,
network: network.clone(),
sandbox_permissions,
windows_sandbox_level,
windows_sandbox_private_desktop,
justification,
arg0,
};
let start = Instant::now();
let raw_output_result = exec(
params,
sandbox,
sandbox_policy,
&file_system_sandbox_policy,
network_sandbox_policy,
stdout_stream,
after_spawn,
)
.await;
let duration = start.elapsed();
finalize_exec_result_bytes(raw_output_result, sandbox, duration)
}
#[cfg(target_os = "windows")]
fn extract_create_process_as_user_error_code(err: &str) -> Option<String> {
let marker = "CreateProcessAsUserW failed: ";
@@ -574,6 +626,64 @@ fn finalize_exec_result(
}
}
fn finalize_exec_result_bytes(
raw_output_result: std::result::Result<RawExecToolCallOutput, CodexErr>,
sandbox_type: SandboxType,
duration: Duration,
) -> Result<ExecToolCallOutputBytes> {
match raw_output_result {
Ok(raw_output) => {
#[allow(unused_mut)]
let mut timed_out = raw_output.timed_out;
#[cfg(target_family = "unix")]
{
if let Some(signal) = raw_output.exit_status.signal() {
if signal == TIMEOUT_CODE {
timed_out = true;
} else {
return Err(CodexErr::Sandbox(SandboxErr::Signal(signal)));
}
}
}
let mut exit_code = raw_output.exit_status.code().unwrap_or(-1);
if timed_out {
exit_code = EXEC_TIMEOUT_EXIT_CODE;
}
let exec_output = ExecToolCallOutputBytes {
exit_code,
stdout: raw_output.stdout,
stderr: raw_output.stderr,
aggregated_output: raw_output.aggregated_output,
duration,
timed_out,
};
if timed_out {
return Err(CodexErr::Sandbox(SandboxErr::Timeout {
output: Box::new(exec_output.to_utf8_lossy_output()),
}));
}
let string_output = exec_output.to_utf8_lossy_output();
if is_likely_sandbox_denied(sandbox_type, &string_output) {
return Err(CodexErr::Sandbox(SandboxErr::Denied {
output: Box::new(string_output),
network_policy_decision: None,
}));
}
Ok(exec_output)
}
Err(err) => {
tracing::error!("exec error: {err}");
Err(err)
}
}
}
pub(crate) mod errors {
use super::CodexErr;
use crate::sandboxing::SandboxTransformError;
@@ -741,6 +851,16 @@ pub struct ExecToolCallOutput {
pub timed_out: bool,
}
#[derive(Clone, Debug)]
pub(crate) struct ExecToolCallOutputBytes {
pub exit_code: i32,
pub stdout: StreamOutput<Vec<u8>>,
pub stderr: StreamOutput<Vec<u8>>,
pub aggregated_output: StreamOutput<Vec<u8>>,
pub duration: Duration,
pub timed_out: bool,
}
impl Default for ExecToolCallOutput {
fn default() -> Self {
Self {
@@ -754,6 +874,19 @@ impl Default for ExecToolCallOutput {
}
}
impl ExecToolCallOutputBytes {
fn to_utf8_lossy_output(&self) -> ExecToolCallOutput {
ExecToolCallOutput {
exit_code: self.exit_code,
stdout: self.stdout.from_utf8_lossy(),
stderr: self.stderr.from_utf8_lossy(),
aggregated_output: self.aggregated_output.from_utf8_lossy(),
duration: self.duration,
timed_out: self.timed_out,
}
}
}
#[cfg_attr(not(target_os = "windows"), allow(unused_variables))]
async fn exec(
params: ExecParams,

View File

@@ -76,16 +76,11 @@ mod shell_detect;
mod stream_events_utils;
pub mod test_support;
mod text_encoding;
pub mod token_data;
mod truncate;
mod unified_exec;
pub mod windows_sandbox;
pub use client::X_RESPONSESAPI_INCLUDE_TIMING_METRICS_HEADER;
pub use codex_auth::token_data::IdTokenInfo;
pub use codex_auth::token_data::IdTokenInfoError;
pub use codex_auth::token_data::KnownPlan;
pub use codex_auth::token_data::PlanType;
pub use codex_auth::token_data::TokenData;
pub use codex_auth::token_data::parse_chatgpt_jwt_claims;
pub use model_provider_info::DEFAULT_LMSTUDIO_PORT;
pub use model_provider_info::DEFAULT_OLLAMA_PORT;
pub use model_provider_info::LMSTUDIO_OSS_PROVIDER_ID;
@@ -119,6 +114,7 @@ pub mod default_client;
pub mod project_doc;
mod rollout;
pub(crate) mod safety;
mod sandboxed_fs;
pub mod seatbelt;
pub mod shell;
pub mod shell_snapshot;

View File

@@ -5,10 +5,10 @@
//! 2. User-defined entries inside `~/.codex/config.toml` under the `model_providers`
//! key. These override or extend the defaults at runtime.
use crate::auth::AuthMode;
use crate::error::EnvVarError;
use codex_api::Provider as ApiProvider;
use codex_api::provider::RetryConfig as ApiRetryConfig;
use codex_app_server_protocol::AuthMode as ApiAuthMode;
use http::HeaderMap;
use http::header::HeaderName;
use http::header::HeaderValue;
@@ -159,12 +159,9 @@ impl ModelProviderInfo {
pub(crate) fn to_api_provider(
&self,
auth_mode: Option<ApiAuthMode>,
auth_mode: Option<AuthMode>,
) -> crate::error::Result<ApiProvider> {
let default_base_url = if matches!(
auth_mode,
Some(ApiAuthMode::Chatgpt | ApiAuthMode::ChatgptAuthTokens)
) {
let default_base_url = if matches!(auth_mode, Some(AuthMode::Chatgpt)) {
"https://chatgpt.com/backend-api/codex"
} else {
"https://api.openai.com/v1"
@@ -267,6 +264,7 @@ impl ModelProviderInfo {
.into_iter()
.collect(),
),
// Use global defaults for retry/timeout unless overridden in config.toml.
request_max_retries: None,
stream_max_retries: None,
stream_idle_timeout_ms: None,
@@ -294,6 +292,10 @@ pub fn built_in_model_providers(
use ModelProviderInfo as P;
let openai_provider = P::create_openai_provider(openai_base_url);
// We do not want to be in the business of adjucating which third-party
// providers are bundled with Codex CLI, so we only include the OpenAI and
// open source ("oss") providers by default. Users are encouraged to add to
// `model_providers` in config.toml to add their own providers.
[
(OPENAI_PROVIDER_ID, openai_provider),
(
@@ -311,6 +313,8 @@ pub fn built_in_model_providers(
}
pub fn create_oss_provider(default_provider_port: u16, wire_api: WireApi) -> ModelProviderInfo {
// These CODEX_OSS_ environment variables are experimental: we may
// switch to reading values from config.toml instead.
let default_codex_oss_base_url = format!(
"http://localhost:{codex_oss_port}/v1",
codex_oss_port = std::env::var("CODEX_OSS_PORT")
@@ -346,3 +350,7 @@ pub fn create_oss_provider_with_base_url(base_url: &str, wire_api: WireApi) -> M
supports_websockets: false,
}
}
#[cfg(test)]
#[path = "model_provider_info_tests.rs"]
mod tests;

View File

@@ -1,4 +1,4 @@
use super::provider::*;
use super::*;
use pretty_assertions::assert_eq;
#[test]

View File

@@ -433,9 +433,7 @@ impl ModelsManager {
codex_otel::start_global_timer("codex.remote_models.fetch_update.duration_ms", &[]);
let auth = self.auth_manager.auth().await;
let auth_mode = auth.as_ref().map(CodexAuth::auth_mode);
let api_provider = self
.provider
.to_api_provider(auth.as_ref().map(CodexAuth::api_auth_mode))?;
let api_provider = self.provider.to_api_provider(auth_mode)?;
let api_auth = auth_provider_from_auth(auth.clone(), &self.provider)?;
let auth_env = collect_auth_env_telemetry(
&self.provider,

View File

@@ -22,7 +22,6 @@ use codex_api::RealtimeSessionMode;
use codex_api::RealtimeWebsocketClient;
use codex_api::endpoint::realtime_websocket::RealtimeWebsocketEvents;
use codex_api::endpoint::realtime_websocket::RealtimeWebsocketWriter;
use codex_app_server_protocol::AuthMode as ApiAuthMode;
use codex_protocol::protocol::CodexErrorInfo;
use codex_protocol::protocol::ConversationAudioParams;
use codex_protocol::protocol::ConversationStartParams;
@@ -455,7 +454,7 @@ async fn prepare_realtime_start(
let provider = sess.provider().await;
let auth = sess.services.auth_manager.auth().await;
let realtime_api_key = realtime_api_key(auth.as_ref(), &provider)?;
let mut api_provider = provider.to_api_provider(Some(ApiAuthMode::ApiKey))?;
let mut api_provider = provider.to_api_provider(Some(crate::auth::AuthMode::ApiKey))?;
let config = sess.get_config().await;
if let Some(realtime_ws_base_url) = &config.experimental_realtime_ws_base_url {
api_provider.base_url = realtime_ws_base_url.clone();

View File

@@ -0,0 +1,197 @@
use crate::codex::Session;
use crate::codex::TurnContext;
use crate::error::CodexErr;
use crate::error::SandboxErr;
use crate::exec::ExecExpiration;
use crate::exec::ExecToolCallOutputBytes;
use crate::sandboxing::CommandSpec;
use crate::sandboxing::SandboxPermissions;
use crate::sandboxing::execute_env_bytes;
use crate::sandboxing::merge_permission_profiles;
use crate::tools::sandboxing::SandboxAttempt;
use crate::tools::sandboxing::SandboxablePreference;
use codex_fs_ops::CODEX_CORE_FS_OPS_ARG1;
use codex_fs_ops::FsError;
use codex_fs_ops::FsErrorKind;
use codex_protocol::models::PermissionProfile;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
const SANDBOXED_FS_TIMEOUT: Duration = Duration::from_secs(30);
pub(crate) async fn read_file(
session: &Arc<Session>,
turn: &Arc<TurnContext>,
path: &Path,
) -> Result<Vec<u8>, SandboxedFsError> {
let output = run_request(session, turn, path).await?;
Ok(output.stdout.text)
}
async fn run_request(
session: &Arc<Session>,
turn: &Arc<TurnContext>,
path: &Path,
) -> Result<ExecToolCallOutputBytes, SandboxedFsError> {
let exe = std::env::current_exe().map_err(|error| SandboxedFsError::ResolveExe {
message: error.to_string(),
})?;
let additional_permissions = effective_granted_permissions(session).await;
let sandbox_manager = crate::sandboxing::SandboxManager::new();
let attempt = SandboxAttempt {
sandbox: sandbox_manager.select_initial(
&turn.file_system_sandbox_policy,
turn.network_sandbox_policy,
SandboxablePreference::Auto,
turn.windows_sandbox_level,
/*has_managed_network_requirements*/ false,
),
policy: &turn.sandbox_policy,
file_system_policy: &turn.file_system_sandbox_policy,
network_policy: turn.network_sandbox_policy,
enforce_managed_network: false,
manager: &sandbox_manager,
sandbox_cwd: &turn.cwd,
codex_linux_sandbox_exe: turn.codex_linux_sandbox_exe.as_ref(),
use_legacy_landlock: turn.features.use_legacy_landlock(),
windows_sandbox_level: turn.windows_sandbox_level,
windows_sandbox_private_desktop: turn.config.permissions.windows_sandbox_private_desktop,
};
let exec_request = attempt
.env_for(
CommandSpec {
program: exe.to_string_lossy().to_string(),
args: vec![
CODEX_CORE_FS_OPS_ARG1.to_string(),
"read".to_string(),
path.to_string_lossy().to_string(),
],
cwd: turn.cwd.clone(),
env: HashMap::new(),
expiration: ExecExpiration::Timeout(SANDBOXED_FS_TIMEOUT),
sandbox_permissions: SandboxPermissions::UseDefault,
additional_permissions,
justification: None,
},
/*network*/ None,
)
.map_err(|error| SandboxedFsError::ProcessFailed {
path: path.to_path_buf(),
exit_code: -1,
message: error.to_string(),
})?;
let output = execute_env_bytes(exec_request, /*stdout_stream*/ None)
.await
.map_err(|error| map_exec_error(path, error))?;
if output.exit_code != 0 {
return Err(parse_helper_failure(
path,
output.exit_code,
&output.stderr.text,
&output.stdout.text,
));
}
Ok(output)
}
async fn effective_granted_permissions(session: &Session) -> Option<PermissionProfile> {
let granted_session_permissions = session.granted_session_permissions().await;
let granted_turn_permissions = session.granted_turn_permissions().await;
merge_permission_profiles(
granted_session_permissions.as_ref(),
granted_turn_permissions.as_ref(),
)
}
fn map_exec_error(path: &Path, error: CodexErr) -> SandboxedFsError {
match error {
CodexErr::Sandbox(SandboxErr::Timeout { .. }) => SandboxedFsError::TimedOut {
path: path.to_path_buf(),
},
_ => SandboxedFsError::ProcessFailed {
path: path.to_path_buf(),
exit_code: -1,
message: error.to_string(),
},
}
}
fn parse_helper_failure(
path: &Path,
exit_code: i32,
stderr: &[u8],
stdout: &[u8],
) -> SandboxedFsError {
if let Ok(error) = serde_json::from_slice::<FsError>(stderr) {
return SandboxedFsError::Operation {
path: path.to_path_buf(),
error,
};
}
let stderr = String::from_utf8_lossy(stderr);
let stdout = String::from_utf8_lossy(stdout);
let message = if !stderr.trim().is_empty() {
stderr.trim().to_string()
} else if !stdout.trim().is_empty() {
stdout.trim().to_string()
} else {
"no error details emitted".to_string()
};
SandboxedFsError::ProcessFailed {
path: path.to_path_buf(),
exit_code,
message,
}
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum SandboxedFsError {
#[error("failed to determine codex executable: {message}")]
ResolveExe { message: String },
#[error("sandboxed fs helper timed out while accessing `{path}`")]
TimedOut { path: PathBuf },
#[error("sandboxed fs helper exited with code {exit_code} while accessing `{path}`: {message}")]
ProcessFailed {
path: PathBuf,
exit_code: i32,
message: String,
},
#[error("sandboxed fs helper could not access `{path}`: {error}")]
Operation { path: PathBuf, error: FsError },
}
impl SandboxedFsError {
pub(crate) fn operation_error_kind(&self) -> Option<&FsErrorKind> {
match self {
Self::Operation { error, .. } => Some(&error.kind),
_ => None,
}
}
pub(crate) fn operation_error_message(&self) -> Option<&str> {
match self {
Self::Operation { error, .. } => Some(error.message.as_str()),
_ => None,
}
}
#[allow(dead_code)]
pub(crate) fn to_io_error(&self) -> std::io::Error {
match self {
Self::Operation { error, .. } => error.to_io_error(),
Self::TimedOut { .. } => {
std::io::Error::new(std::io::ErrorKind::TimedOut, self.to_string())
}
Self::ResolveExe { .. } | Self::ProcessFailed { .. } => {
std::io::Error::other(self.to_string())
}
}
}
}

View File

@@ -10,9 +10,11 @@ pub(crate) mod macos_permissions;
use crate::exec::ExecExpiration;
use crate::exec::ExecToolCallOutput;
use crate::exec::ExecToolCallOutputBytes;
use crate::exec::SandboxType;
use crate::exec::StdoutStream;
use crate::exec::execute_exec_request;
use crate::exec::execute_exec_request_bytes;
use crate::landlock::allow_network_for_proxy;
use crate::landlock::create_linux_sandbox_command_args_for_policies;
use crate::protocol::SandboxPolicy;
@@ -738,6 +740,20 @@ pub async fn execute_env(
.await
}
pub(crate) async fn execute_env_bytes(
exec_request: ExecRequest,
stdout_stream: Option<StdoutStream>,
) -> crate::error::Result<ExecToolCallOutputBytes> {
let effective_policy = exec_request.sandbox_policy.clone();
execute_exec_request_bytes(
exec_request,
&effective_policy,
stdout_stream,
/*after_spawn*/ None,
)
.await
}
pub async fn execute_exec_request_with_after_spawn(
exec_request: ExecRequest,
stdout_stream: Option<StdoutStream>,

View File

@@ -5,21 +5,32 @@ use thiserror::Error;
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Default)]
pub struct TokenData {
/// Flat info parsed from the JWT in auth.json.
#[serde(
deserialize_with = "deserialize_id_token",
serialize_with = "serialize_id_token"
)]
pub id_token: IdTokenInfo,
/// This is a JWT.
pub access_token: String,
pub refresh_token: String,
pub account_id: Option<String>,
}
/// Flat subset of useful claims in id_token from auth.json.
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct IdTokenInfo {
pub email: Option<String>,
pub chatgpt_plan_type: Option<PlanType>,
/// The ChatGPT subscription plan type
/// (e.g., "free", "plus", "pro", "business", "enterprise", "edu").
/// (Note: values may vary by backend.)
pub(crate) chatgpt_plan_type: Option<PlanType>,
/// ChatGPT user identifier associated with the token, if present.
pub chatgpt_user_id: Option<String>,
/// Organization/workspace identifier associated with the token, if present.
pub chatgpt_account_id: Option<String>,
pub raw_jwt: String,
}
@@ -44,13 +55,13 @@ impl IdTokenInfo {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PlanType {
pub(crate) enum PlanType {
Known(KnownPlan),
Unknown(String),
}
impl PlanType {
pub fn from_raw_value(raw: &str) -> Self {
pub(crate) fn from_raw_value(raw: &str) -> Self {
match raw.to_ascii_lowercase().as_str() {
"free" => Self::Known(KnownPlan::Free),
"go" => Self::Known(KnownPlan::Go),
@@ -67,7 +78,7 @@ impl PlanType {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum KnownPlan {
pub(crate) enum KnownPlan {
Free,
Go,
Plus,
@@ -117,6 +128,7 @@ pub enum IdTokenInfoError {
}
pub fn parse_chatgpt_jwt_claims(jwt: &str) -> Result<IdTokenInfo, IdTokenInfoError> {
// JWT format: header.payload.signature
let mut parts = jwt.split('.');
let (_header_b64, payload_b64, _sig_b64) = match (parts.next(), parts.next(), parts.next()) {
(Some(h), Some(p), Some(s)) if !h.is_empty() && !p.is_empty() && !s.is_empty() => (h, p, s),
@@ -161,3 +173,7 @@ where
{
serializer.serialize_str(&id_token.raw_jwt)
}
#[cfg(test)]
#[path = "token_data_tests.rs"]
mod tests;

View File

@@ -1,5 +1,4 @@
use super::token_data::*;
use base64::Engine;
use super::*;
use pretty_assertions::assert_eq;
use serde::Serialize;

View File

@@ -1,5 +1,4 @@
use async_trait::async_trait;
use codex_environment::ExecutorFileSystem;
use codex_protocol::models::ContentItem;
use codex_protocol::models::FunctionCallOutputContentItem;
use codex_protocol::models::ImageDetail;
@@ -13,6 +12,7 @@ use crate::function_tool::FunctionCallError;
use crate::original_image_detail::can_request_original_image_detail;
use crate::protocol::EventMsg;
use crate::protocol::ViewImageToolCallEvent;
use crate::sandboxed_fs;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
@@ -92,36 +92,6 @@ impl ToolHandler for ViewImageHandler {
AbsolutePathBuf::try_from(turn.resolve_path(Some(args.path))).map_err(|error| {
FunctionCallError::RespondToModel(format!("unable to resolve image path: {error}"))
})?;
let metadata = turn
.environment
.get_filesystem()
.get_metadata(&abs_path)
.await
.map_err(|error| {
FunctionCallError::RespondToModel(format!(
"unable to locate image at `{}`: {error}",
abs_path.display()
))
})?;
if !metadata.is_file {
return Err(FunctionCallError::RespondToModel(format!(
"image path `{}` is not a file",
abs_path.display()
)));
}
let file_bytes = turn
.environment
.get_filesystem()
.read_file(&abs_path)
.await
.map_err(|error| {
FunctionCallError::RespondToModel(format!(
"unable to read image at `{}`: {error}",
abs_path.display()
))
})?;
let event_path = abs_path.to_path_buf();
let can_request_original_detail =
@@ -134,10 +104,18 @@ impl ToolHandler for ViewImageHandler {
PromptImageMode::ResizeToFit
};
let image_detail = use_original_detail.then_some(ImageDetail::Original);
let image_bytes = sandboxed_fs::read_file(&session, &turn, abs_path.as_path())
.await
.map_err(|error| {
FunctionCallError::RespondToModel(render_view_image_read_error(
abs_path.as_path(),
&error,
))
})?;
let content = local_image_content_items_with_label_number(
abs_path.as_path(),
file_bytes,
image_bytes,
/*label_number*/ None,
image_mode,
)
@@ -165,3 +143,30 @@ impl ToolHandler for ViewImageHandler {
Ok(FunctionToolOutput::from_content(content, Some(true)))
}
}
fn render_view_image_read_error(
path: &std::path::Path,
error: &sandboxed_fs::SandboxedFsError,
) -> String {
let operation_message = error
.operation_error_message()
.map(str::to_owned)
.unwrap_or_else(|| error.to_string());
match error.operation_error_kind() {
Some(codex_fs_ops::FsErrorKind::IsADirectory) => {
format!("image path `{}` is not a file", path.display())
}
Some(codex_fs_ops::FsErrorKind::NotFound) => {
format!(
"unable to locate image at `{}`: {operation_message}",
path.display()
)
}
Some(_) | None => {
format!(
"unable to read image at `{}`: {operation_message}",
path.display()
)
}
}
}

View File

@@ -5,8 +5,6 @@ use chrono::Duration;
use chrono::Utc;
use codex_app_server_protocol::AuthMode;
use codex_core::AuthManager;
use codex_core::IdTokenInfo;
use codex_core::TokenData;
use codex_core::auth::AuthCredentialsStoreMode;
use codex_core::auth::AuthDotJson;
use codex_core::auth::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
@@ -14,6 +12,8 @@ use codex_core::auth::RefreshTokenError;
use codex_core::auth::load_auth_dot_json;
use codex_core::auth::save_auth;
use codex_core::error::RefreshTokenFailedReason;
use codex_core::token_data::IdTokenInfo;
use codex_core::token_data::TokenData;
use core_test_support::skip_if_no_network;
use pretty_assertions::assert_eq;
use serde::Serialize;

View File

@@ -3,6 +3,7 @@
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use codex_core::CodexAuth;
use codex_core::config::Constrained;
use codex_core::features::Feature;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::openai_models::ConfigShellToolType;
@@ -13,9 +14,15 @@ use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::openai_models::ReasoningEffortPreset;
use codex_protocol::openai_models::TruncationPolicyConfig;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::ReadOnlyAccess;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::user_input::UserInput;
use core_test_support::responses;
@@ -1244,6 +1251,109 @@ async fn view_image_tool_errors_when_file_missing() -> anyhow::Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn view_image_tool_respects_filesystem_sandbox() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let sandbox_policy_for_config = SandboxPolicy::ReadOnly {
access: ReadOnlyAccess::Restricted {
include_platform_defaults: true,
readable_roots: Vec::new(),
},
network_access: false,
};
let mut builder = test_codex().with_config({
let sandbox_policy_for_config = sandbox_policy_for_config.clone();
move |config| {
config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config);
config.permissions.file_system_sandbox_policy =
FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Minimal,
},
access: FileSystemAccessMode::Read,
},
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::CurrentWorkingDirectory,
},
access: FileSystemAccessMode::Read,
},
]);
}
});
let TestCodex {
codex,
config,
cwd,
session_configured,
..
} = builder.build(&server).await?;
let outside_dir = tempfile::tempdir()?;
let abs_path = outside_dir.path().join("blocked.png");
let image = ImageBuffer::from_pixel(256, 128, Rgba([10u8, 20, 30, 255]));
image.save(&abs_path)?;
let call_id = "view-image-sandbox-denied";
let arguments = serde_json::json!({ "path": abs_path }).to_string();
let first_response = sse(vec![
ev_response_created("resp-1"),
ev_function_call(call_id, "view_image", &arguments),
ev_completed("resp-1"),
]);
responses::mount_sse_once(&server, first_response).await;
let second_response = sse(vec![
ev_assistant_message("msg-1", "done"),
ev_completed("resp-2"),
]);
let mock = responses::mount_sse_once(&server, second_response).await;
let session_model = session_configured.model.clone();
codex
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "please attach the outside image".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
model: session_model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
let request = mock.single_request();
assert!(
request.inputs_of_type("input_image").is_empty(),
"sandbox-denied image should not produce an input_image message"
);
let output_text = request
.function_call_output_content_and_success(call_id)
.and_then(|(content, _)| content)
.expect("output text present");
let expected_prefix = format!("unable to read image at `{}`:", abs_path.display());
assert!(
output_text.starts_with(&expected_prefix),
"expected sandbox denial prefix `{expected_prefix}` but got `{output_text}`"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));

View File

@@ -1,6 +1,6 @@
load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "codex-auth",
crate_name = "codex_auth",
name = "fs-ops",
crate_name = "codex_fs_ops",
)

View File

@@ -1,23 +1,22 @@
[package]
name = "codex-auth"
name = "codex-fs-ops"
version.workspace = true
edition.workspace = true
license.workspace = true
[lib]
name = "codex_fs_ops"
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
anyhow = { workspace = true }
base64 = { workspace = true }
codex-api = { workspace = true }
codex-app-server-protocol = { workspace = true }
http = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
[dev-dependencies]
maplit = { workspace = true }
pretty_assertions = { workspace = true }
toml = { workspace = true }
tempfile = { workspace = true }

View File

@@ -0,0 +1,38 @@
use std::ffi::OsString;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FsCommand {
ReadFile { path: PathBuf },
}
pub fn parse_command_from_args(
mut args: impl Iterator<Item = OsString>,
) -> Result<FsCommand, String> {
let Some(operation) = args.next() else {
return Err("missing operation".to_string());
};
let Some(operation) = operation.to_str() else {
return Err("operation must be valid UTF-8".to_string());
};
let Some(path) = args.next() else {
return Err(format!("missing path for operation `{operation}`"));
};
if args.next().is_some() {
return Err(format!(
"unexpected extra arguments for operation `{operation}`"
));
}
let path = PathBuf::from(path);
match operation {
"read" => Ok(FsCommand::ReadFile { path }),
_ => Err(format!(
"unsupported filesystem operation `{operation}`; expected `read`"
)),
}
}
#[cfg(test)]
#[path = "command_tests.rs"]
mod tests;

View File

@@ -0,0 +1,16 @@
use super::FsCommand;
use super::parse_command_from_args;
use pretty_assertions::assert_eq;
#[test]
fn parse_read_command() {
let command = parse_command_from_args(["read", "/tmp/example.png"].into_iter().map(Into::into))
.expect("command should parse");
assert_eq!(
command,
FsCommand::ReadFile {
path: "/tmp/example.png".into(),
}
);
}

View File

@@ -0,0 +1,3 @@
/// Special argv[1] flag used when the Codex executable self-invokes to run the
/// internal sandbox-backed filesystem helper path.
pub const CODEX_CORE_FS_OPS_ARG1: &str = "--codex-run-as-fs-ops";

View File

@@ -0,0 +1,70 @@
use serde::Deserialize;
use serde::Serialize;
use std::io::ErrorKind;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FsErrorKind {
NotFound,
PermissionDenied,
IsADirectory,
InvalidData,
Other,
}
impl From<ErrorKind> for FsErrorKind {
fn from(value: ErrorKind) -> Self {
match value {
ErrorKind::NotFound => Self::NotFound,
ErrorKind::PermissionDenied => Self::PermissionDenied,
ErrorKind::IsADirectory => Self::IsADirectory,
ErrorKind::InvalidData => Self::InvalidData,
_ => Self::Other,
}
}
}
impl FsErrorKind {
pub fn to_io_error_kind(&self) -> ErrorKind {
match self {
Self::NotFound => ErrorKind::NotFound,
Self::PermissionDenied => ErrorKind::PermissionDenied,
Self::IsADirectory => ErrorKind::IsADirectory,
Self::InvalidData => ErrorKind::InvalidData,
Self::Other => ErrorKind::Other,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FsError {
pub kind: FsErrorKind,
pub message: String,
pub raw_os_error: Option<i32>,
}
impl std::fmt::Display for FsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
impl FsError {
pub fn to_io_error(&self) -> std::io::Error {
if let Some(raw_os_error) = self.raw_os_error {
std::io::Error::from_raw_os_error(raw_os_error)
} else {
std::io::Error::new(self.kind.to_io_error_kind(), self.message.clone())
}
}
}
impl From<std::io::Error> for FsError {
fn from(error: std::io::Error) -> Self {
Self {
kind: error.kind().into(),
message: error.to_string(),
raw_os_error: error.raw_os_error(),
}
}
}

View File

@@ -0,0 +1,13 @@
mod command;
mod constants;
mod error;
mod runner;
pub use command::FsCommand;
pub use command::parse_command_from_args;
pub use constants::CODEX_CORE_FS_OPS_ARG1;
pub use error::FsError;
pub use error::FsErrorKind;
pub use runner::execute;
pub use runner::run_from_args;
pub use runner::write_error;

View File

@@ -0,0 +1,55 @@
use crate::FsCommand;
use crate::FsError;
use crate::parse_command_from_args;
use anyhow::Context;
use anyhow::Result;
use std::ffi::OsString;
use std::io::Read;
use std::io::Write;
pub fn run_from_args(
args: impl Iterator<Item = OsString>,
stdin: &mut impl Read,
stdout: &mut impl Write,
stderr: &mut impl Write,
) -> Result<()> {
let command = match parse_command_from_args(args) {
Ok(command) => command,
Err(error) => {
writeln!(stderr, "{error}").context("failed to write fs helper usage error")?;
anyhow::bail!("{error}");
}
};
if let Err(error) = execute(command, stdin, stdout) {
write_error(stderr, &error)?;
anyhow::bail!("{error}");
}
Ok(())
}
pub fn execute(
command: FsCommand,
_stdin: &mut impl Read,
stdout: &mut impl Write,
) -> Result<(), FsError> {
match command {
FsCommand::ReadFile { path } => {
let mut file = std::fs::File::open(path).map_err(FsError::from)?;
std::io::copy(&mut file, stdout)
.map(|_| ())
.map_err(FsError::from)
}
}
}
pub fn write_error(stderr: &mut impl Write, error: &FsError) -> Result<()> {
serde_json::to_writer(&mut *stderr, error).context("failed to serialize fs error")?;
writeln!(stderr).context("failed to terminate fs error with newline")?;
Ok(())
}
#[cfg(test)]
#[path = "runner_tests.rs"]
mod tests;

View File

@@ -0,0 +1,76 @@
use super::execute;
use crate::FsCommand;
use crate::FsErrorKind;
use crate::run_from_args;
use pretty_assertions::assert_eq;
use tempfile::tempdir;
#[test]
fn run_from_args_streams_file_bytes_to_stdout() {
let tempdir = tempdir().expect("tempdir");
let path = tempdir.path().join("image.bin");
let expected = b"hello\x00world".to_vec();
std::fs::write(&path, &expected).expect("write test file");
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let mut stdin = std::io::empty();
run_from_args(
["read", path.to_str().expect("utf-8 test path")]
.into_iter()
.map(Into::into),
&mut stdin,
&mut stdout,
&mut stderr,
)
.expect("read should succeed");
assert_eq!(stdout, expected);
assert_eq!(stderr, Vec::<u8>::new());
}
#[test]
fn read_reports_directory_error() {
let tempdir = tempdir().expect("tempdir");
let mut stdout = Vec::new();
let mut stdin = std::io::empty();
let error = execute(
FsCommand::ReadFile {
path: tempdir.path().to_path_buf(),
},
&mut stdin,
&mut stdout,
)
.expect_err("reading a directory should fail");
#[cfg(target_os = "windows")]
assert_eq!(error.kind, FsErrorKind::PermissionDenied);
#[cfg(not(target_os = "windows"))]
assert_eq!(error.kind, FsErrorKind::IsADirectory);
}
#[test]
fn run_from_args_serializes_errors_to_stderr() {
let tempdir = tempdir().expect("tempdir");
let missing = tempdir.path().join("missing.txt");
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let mut stdin = std::io::empty();
let result = run_from_args(
["read", missing.to_str().expect("utf-8 test path")]
.into_iter()
.map(Into::into),
&mut stdin,
&mut stdout,
&mut stderr,
);
assert!(result.is_err(), "missing file should fail");
assert_eq!(stdout, Vec::<u8>::new());
let error: crate::FsError = serde_json::from_slice(&stderr).expect("structured fs error");
assert_eq!(error.kind, FsErrorKind::NotFound);
assert!(error.raw_os_error.is_some());
}

View File

@@ -16,7 +16,6 @@ pub use server::run_login_server;
pub use codex_app_server_protocol::AuthMode;
pub use codex_core::AuthManager;
pub use codex_core::CodexAuth;
pub use codex_core::TokenData;
pub use codex_core::auth::AuthDotJson;
pub use codex_core::auth::CLIENT_ID;
pub use codex_core::auth::CODEX_API_KEY_ENV_VAR;
@@ -24,3 +23,4 @@ pub use codex_core::auth::OPENAI_API_KEY_ENV_VAR;
pub use codex_core::auth::login_with_api_key;
pub use codex_core::auth::logout;
pub use codex_core::auth::save_auth;
pub use codex_core::token_data::TokenData;

View File

@@ -29,12 +29,12 @@ use base64::Engine;
use chrono::Utc;
use codex_app_server_protocol::AuthMode;
use codex_client::build_reqwest_client_with_custom_ca;
use codex_core::TokenData;
use codex_core::auth::AuthCredentialsStoreMode;
use codex_core::auth::AuthDotJson;
use codex_core::auth::save_auth;
use codex_core::default_client::originator;
use codex_core::parse_chatgpt_jwt_claims;
use codex_core::token_data::TokenData;
use codex_core::token_data::parse_chatgpt_jwt_claims;
use rand::RngCore;
use serde_json::Value as JsonValue;
use tiny_http::Header;

View File

@@ -2,10 +2,8 @@ use std::path::Path;
use codex_app_server_protocol::AuthMode;
use codex_app_server_protocol::ChatgptAuthTokensRefreshResponse;
use codex_core::TokenData;
use codex_core::auth::AuthCredentialsStoreMode;
use codex_core::auth::load_auth_dot_json;
use codex_core::parse_chatgpt_jwt_claims;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct LocalChatgptAuth {
@@ -71,10 +69,10 @@ mod tests {
use base64::Engine;
use chrono::Utc;
use codex_app_server_protocol::AuthMode;
use codex_core::TokenData;
use codex_core::auth::AuthDotJson;
use codex_core::auth::login_with_chatgpt_auth_tokens;
use codex_core::auth::save_auth;
use codex_core::token_data::TokenData;
use pretty_assertions::assert_eq;
use serde::Serialize;
use serde_json::json;
@@ -112,7 +110,8 @@ mod tests {
auth_mode: Some(AuthMode::Chatgpt),
openai_api_key: None,
tokens: Some(TokenData {
id_token: parse_chatgpt_jwt_claims(&id_token).expect("id token should parse"),
id_token: codex_core::token_data::parse_chatgpt_jwt_claims(&id_token)
.expect("id token should parse"),
access_token,
refresh_token: "refresh-token".to_string(),
account_id: Some("workspace-1".to_string()),

View File

@@ -59,7 +59,6 @@ pub fn load_for_prompt_bytes(
mode: PromptImageMode,
) -> Result<EncodedImage, ImageProcessingError> {
let path_buf = path.to_path_buf();
let key = ImageCacheKey {
digest: sha1_digest(&file_bytes),
mode,