mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
## Stack This is PR 3 of the simplified HAI single-run-task stack: - [#19047](https://github.com/openai/codex/pull/19047) Agent Identity assertion and task-registration primitives, including the shared run-task helper used by existing Agent Identity JWT auth. - [#19049](https://github.com/openai/codex/pull/19049) Disabled-by-default ChatGPT auth opt-in that provisions/reuses persisted Agent Identity runtime auth and its single run task. - [#19051](https://github.com/openai/codex/pull/19051) Run-scoped provider auth that uses one backend-owned task id for first-party inference and compaction requests. [#19054](https://github.com/openai/codex/pull/19054) collapsed out of the active stack because the simplified design no longer needs a separate background/control-plane task helper. ## Summary This PR moves Agent Identity usage into provider auth resolution. That keeps `AgentAssertion` auth tied to first-party OpenAI provider requests instead of applying a late session-wide override that could affect local, custom, Bedrock, API-key, or external-bearer providers. What changed: - adds a small `ProviderAuthScope` struct carrying the run auth policy and session source needed by provider-scoped auth resolution - lets `Session` opt the existing `ModelClient` into `ChatGptAuth` policy when `use_agent_identity` is enabled, without adding a second model-client constructor - resolves Agent Identity only for first-party OpenAI provider auth paths - uses the persisted run task id from the `AgentIdentityAuth` record to build `AgentAssertion` auth for Responses requests - routes shared request setup through scoped provider auth so unary compact requests use the same run-task assertion path as inference turns - keeps local/custom/Bedrock/env-key/external-bearer provider auth unchanged - lets missing run-task state surface through the existing model-request error path instead of silently falling back to bearer auth This PR intentionally does not create thread-scoped, target-scoped, or background-scoped task identities. The run task is the only task Codex registers in this POC shape. ## Testing - `just test -p codex-model-provider` - `just test -p codex-core client::tests::provider_auth_scope_uses` - `just test -p codex-core remote_compact_uses_agent_identity_assertion`
271 lines
11 KiB
Rust
271 lines
11 KiB
Rust
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use codex_api::AgentIdentityTelemetry;
|
|
use codex_api::ModelsClient;
|
|
use codex_api::RequestTelemetry;
|
|
use codex_api::ReqwestTransport;
|
|
use codex_api::TransportError;
|
|
use codex_api::auth_header_telemetry;
|
|
use codex_api::map_api_error;
|
|
use codex_feedback::FeedbackRequestTags;
|
|
use codex_feedback::emit_feedback_request_tags_with_auth_env;
|
|
use codex_login::AuthEnvTelemetry;
|
|
use codex_login::AuthManager;
|
|
use codex_login::CodexAuth;
|
|
use codex_login::collect_auth_env_telemetry;
|
|
use codex_login::default_client::build_reqwest_client;
|
|
use codex_model_provider_info::ModelProviderInfo;
|
|
use codex_models_manager::manager::ModelsEndpointClient;
|
|
use codex_models_manager::manager::ModelsEndpointFuture;
|
|
use codex_otel::TelemetryAuthMode;
|
|
use codex_protocol::error::CodexErr;
|
|
use codex_protocol::error::Result as CoreResult;
|
|
use codex_protocol::openai_models::ModelInfo;
|
|
use codex_response_debug_context::extract_response_debug_context;
|
|
use codex_response_debug_context::telemetry_transport_error_message;
|
|
use http::HeaderMap;
|
|
use tokio::time::timeout;
|
|
|
|
use crate::auth::agent_identity_telemetry;
|
|
use crate::auth::resolve_provider_auth;
|
|
|
|
const MODELS_REFRESH_TIMEOUT: Duration = Duration::from_secs(5);
|
|
const MODELS_ENDPOINT: &str = "/models";
|
|
|
|
/// Provider-owned OpenAI-compatible `/models` endpoint.
|
|
#[derive(Debug)]
|
|
pub(crate) struct OpenAiModelsEndpoint {
|
|
provider_info: ModelProviderInfo,
|
|
auth_manager: Option<Arc<AuthManager>>,
|
|
}
|
|
|
|
impl OpenAiModelsEndpoint {
|
|
pub(crate) fn new(
|
|
provider_info: ModelProviderInfo,
|
|
auth_manager: Option<Arc<AuthManager>>,
|
|
) -> Self {
|
|
Self {
|
|
provider_info,
|
|
auth_manager,
|
|
}
|
|
}
|
|
|
|
async fn auth(&self) -> Option<CodexAuth> {
|
|
match self.auth_manager.as_ref() {
|
|
Some(auth_manager) => auth_manager.auth().await,
|
|
None => None,
|
|
}
|
|
}
|
|
|
|
async fn uses_codex_backend(&self) -> bool {
|
|
self.auth()
|
|
.await
|
|
.as_ref()
|
|
.is_some_and(CodexAuth::uses_codex_backend)
|
|
}
|
|
|
|
async fn list_models(
|
|
&self,
|
|
client_version: &str,
|
|
) -> CoreResult<(Vec<ModelInfo>, Option<String>)> {
|
|
let _timer =
|
|
codex_otel::start_global_timer("codex.remote_models.fetch_update.duration_ms", &[]);
|
|
let auth = self.auth().await;
|
|
let auth_mode = auth.as_ref().map(CodexAuth::auth_mode);
|
|
let api_provider = self.provider_info.to_api_provider(auth_mode)?;
|
|
let api_auth = resolve_provider_auth(auth.as_ref(), &self.provider_info)?;
|
|
let transport = ReqwestTransport::new(build_reqwest_client());
|
|
let auth_telemetry = auth_header_telemetry(api_auth.as_ref());
|
|
let agent_identity_telemetry = if let Some(CodexAuth::AgentIdentity(auth)) = auth.as_ref() {
|
|
Some(agent_identity_telemetry(auth))
|
|
} else {
|
|
None
|
|
};
|
|
let request_telemetry: Arc<dyn RequestTelemetry> = Arc::new(ModelsRequestTelemetry {
|
|
auth_mode: auth_mode.map(|mode| TelemetryAuthMode::from(mode).to_string()),
|
|
auth_header_attached: auth_telemetry.attached,
|
|
auth_header_name: auth_telemetry.name,
|
|
agent_identity_telemetry,
|
|
auth_env: self.auth_env(),
|
|
});
|
|
let client = ModelsClient::new(transport, api_provider, api_auth)
|
|
.with_telemetry(Some(request_telemetry));
|
|
|
|
timeout(
|
|
MODELS_REFRESH_TIMEOUT,
|
|
client.list_models(client_version, HeaderMap::new()),
|
|
)
|
|
.await
|
|
.map_err(|_| CodexErr::Timeout)?
|
|
.map_err(map_api_error)
|
|
}
|
|
|
|
fn auth_env(&self) -> AuthEnvTelemetry {
|
|
let codex_api_key_env_enabled = self
|
|
.auth_manager
|
|
.as_ref()
|
|
.is_some_and(|auth_manager| auth_manager.codex_api_key_env_enabled());
|
|
collect_auth_env_telemetry(&self.provider_info, codex_api_key_env_enabled)
|
|
}
|
|
}
|
|
|
|
impl ModelsEndpointClient for OpenAiModelsEndpoint {
|
|
fn has_command_auth(&self) -> bool {
|
|
self.provider_info.has_command_auth()
|
|
}
|
|
|
|
fn uses_codex_backend(&self) -> ModelsEndpointFuture<'_, bool> {
|
|
Box::pin(OpenAiModelsEndpoint::uses_codex_backend(self))
|
|
}
|
|
|
|
fn list_models<'a>(
|
|
&'a self,
|
|
client_version: &'a str,
|
|
) -> ModelsEndpointFuture<'a, CoreResult<(Vec<ModelInfo>, Option<String>)>> {
|
|
Box::pin(OpenAiModelsEndpoint::list_models(self, client_version))
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct ModelsRequestTelemetry {
|
|
auth_mode: Option<String>,
|
|
auth_header_attached: bool,
|
|
auth_header_name: Option<&'static str>,
|
|
agent_identity_telemetry: Option<AgentIdentityTelemetry>,
|
|
auth_env: AuthEnvTelemetry,
|
|
}
|
|
|
|
impl RequestTelemetry for ModelsRequestTelemetry {
|
|
fn on_request(
|
|
&self,
|
|
attempt: u64,
|
|
status: Option<http::StatusCode>,
|
|
error: Option<&TransportError>,
|
|
duration: Duration,
|
|
) {
|
|
let success = status.is_some_and(|code| code.is_success()) && error.is_none();
|
|
let error_message = error.map(telemetry_transport_error_message);
|
|
let response_debug = error
|
|
.map(extract_response_debug_context)
|
|
.unwrap_or_default();
|
|
let status = status.map(|status| status.as_u16());
|
|
tracing::event!(
|
|
target: "codex_otel.log_only",
|
|
tracing::Level::INFO,
|
|
event.name = "codex.api_request",
|
|
duration_ms = %duration.as_millis(),
|
|
http.response.status_code = status,
|
|
success = success,
|
|
error.message = error_message.as_deref(),
|
|
attempt = attempt,
|
|
endpoint = MODELS_ENDPOINT,
|
|
auth.header_attached = self.auth_header_attached,
|
|
auth.header_name = self.auth_header_name,
|
|
auth.env_openai_api_key_present = self.auth_env.openai_api_key_env_present,
|
|
auth.env_codex_api_key_present = self.auth_env.codex_api_key_env_present,
|
|
auth.env_codex_api_key_enabled = self.auth_env.codex_api_key_env_enabled,
|
|
auth.env_provider_key_name = self.auth_env.provider_env_key_name.as_deref(),
|
|
auth.env_provider_key_present = self.auth_env.provider_env_key_present,
|
|
auth.env_refresh_token_url_override_present = self.auth_env.refresh_token_url_override_present,
|
|
auth.request_id = response_debug.request_id.as_deref(),
|
|
auth.cf_ray = response_debug.cf_ray.as_deref(),
|
|
auth.error = response_debug.auth_error.as_deref(),
|
|
auth.error_code = response_debug.auth_error_code.as_deref(),
|
|
auth.mode = self.auth_mode.as_deref(),
|
|
auth.agent_id = self.agent_identity_telemetry.as_ref().map(|metadata| metadata.agent_id.as_str()),
|
|
auth.task_id = self.agent_identity_telemetry.as_ref().map(|metadata| metadata.task_id.as_str()),
|
|
);
|
|
tracing::event!(
|
|
target: "codex_otel.trace_safe",
|
|
tracing::Level::INFO,
|
|
event.name = "codex.api_request",
|
|
duration_ms = %duration.as_millis(),
|
|
http.response.status_code = status,
|
|
success = success,
|
|
error.message = error_message.as_deref(),
|
|
attempt = attempt,
|
|
endpoint = MODELS_ENDPOINT,
|
|
auth.header_attached = self.auth_header_attached,
|
|
auth.header_name = self.auth_header_name,
|
|
auth.env_openai_api_key_present = self.auth_env.openai_api_key_env_present,
|
|
auth.env_codex_api_key_present = self.auth_env.codex_api_key_env_present,
|
|
auth.env_codex_api_key_enabled = self.auth_env.codex_api_key_env_enabled,
|
|
auth.env_provider_key_name = self.auth_env.provider_env_key_name.as_deref(),
|
|
auth.env_provider_key_present = self.auth_env.provider_env_key_present,
|
|
auth.env_refresh_token_url_override_present = self.auth_env.refresh_token_url_override_present,
|
|
auth.request_id = response_debug.request_id.as_deref(),
|
|
auth.cf_ray = response_debug.cf_ray.as_deref(),
|
|
auth.error = response_debug.auth_error.as_deref(),
|
|
auth.error_code = response_debug.auth_error_code.as_deref(),
|
|
auth.mode = self.auth_mode.as_deref(),
|
|
auth.agent_id = self.agent_identity_telemetry.as_ref().map(|metadata| metadata.agent_id.as_str()),
|
|
auth.task_id = self.agent_identity_telemetry.as_ref().map(|metadata| metadata.task_id.as_str()),
|
|
);
|
|
emit_feedback_request_tags_with_auth_env(
|
|
&FeedbackRequestTags {
|
|
endpoint: MODELS_ENDPOINT,
|
|
auth_header_attached: self.auth_header_attached,
|
|
auth_header_name: self.auth_header_name,
|
|
auth_mode: self.auth_mode.as_deref(),
|
|
auth_retry_after_unauthorized: None,
|
|
auth_recovery_mode: None,
|
|
auth_recovery_phase: None,
|
|
auth_connection_reused: None,
|
|
auth_request_id: response_debug.request_id.as_deref(),
|
|
auth_cf_ray: response_debug.cf_ray.as_deref(),
|
|
auth_error: response_debug.auth_error.as_deref(),
|
|
auth_error_code: response_debug.auth_error_code.as_deref(),
|
|
auth_recovery_followup_success: None,
|
|
auth_recovery_followup_status: None,
|
|
},
|
|
&self.auth_env,
|
|
);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::num::NonZeroU64;
|
|
|
|
use super::*;
|
|
use codex_protocol::config_types::ModelProviderAuthInfo;
|
|
|
|
fn provider_info_with_command_auth() -> ModelProviderInfo {
|
|
ModelProviderInfo {
|
|
auth: Some(ModelProviderAuthInfo {
|
|
command: "print-token".to_string(),
|
|
args: Vec::new(),
|
|
timeout_ms: NonZeroU64::new(5_000).expect("timeout should be non-zero"),
|
|
refresh_interval_ms: 300_000,
|
|
cwd: std::env::current_dir()
|
|
.expect("current dir should be available")
|
|
.try_into()
|
|
.expect("current dir should be absolute"),
|
|
}),
|
|
requires_openai_auth: false,
|
|
..ModelProviderInfo::create_openai_provider(/*base_url*/ None)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn command_auth_provider_reports_command_auth_without_cached_auth() {
|
|
let endpoint = OpenAiModelsEndpoint::new(
|
|
provider_info_with_command_auth(),
|
|
/*auth_manager*/ None,
|
|
);
|
|
|
|
assert!(endpoint.has_command_auth());
|
|
}
|
|
|
|
#[test]
|
|
fn provider_without_command_auth_reports_no_command_auth() {
|
|
let endpoint = OpenAiModelsEndpoint::new(
|
|
ModelProviderInfo::create_openai_provider(/*base_url*/ None),
|
|
/*auth_manager*/ None,
|
|
);
|
|
|
|
assert!(!endpoint.has_command_auth());
|
|
}
|
|
}
|