[codex] Use AgentAssertion downstream behind use_agent_identity (#17980)

## Summary

This is the AgentAssertion downstream slice for feature-gated agent
identity support, replacing the oversized AgentAssertion slice from PR
#17807.

It isolates task-scoped downstream AgentAssertion wiring on top of the
merged PR3.1 work without re-carrying the earlier agent registration,
task registration, or task-state history.

This PR includes the task-scoped bug-fix call sites from the review:
generic file upload auth, MCP OpenAI file upload auth, and ARC monitor
auth. Broader user/control-plane calls move to PR4.1 and PR4.2.

## Stack

- PR1: https://github.com/openai/codex/pull/17385 - add
`features.use_agent_identity`
- PR2: https://github.com/openai/codex/pull/17386 - register agent
identities when enabled
- PR3: https://github.com/openai/codex/pull/17387 - register agent tasks
when enabled
- PR3.1: https://github.com/openai/codex/pull/17978 - persist and
prewarm registered tasks per thread
- PR4: this PR - use task-scoped `AgentAssertion` downstream when
enabled
- PR4.1: https://github.com/openai/codex/pull/18094 - introduce
AuthManager-owned background/control-plane `AgentAssertion` auth
- PR4.2: https://github.com/openai/codex/pull/18260 - use background
task auth for additional backend/control-plane calls

## What Changed

- add AgentAssertion envelope generation in `codex-core`
- route downstream HTTP and websocket auth through AgentAssertion when
an agent task is present
- extend the model-provider auth provider so non-bearer authorization
schemes can be passed through cleanly
- make generic file uploads attach the full authorization header value
- make MCP OpenAI file uploads use the cached thread agent task
assertion when present
- make ARC monitor calls use the cached thread agent task assertion when
present

## Why

The original PR had drifted ancestry and showed a much larger diff than
the semantic change actually required. Restacking it onto PR3.1 keeps
the reviewable surface down to the downstream assertion slice.

## Validation

- `just fmt`
- `cargo check -p codex-core -p codex-login -p codex-analytics -p
codex-app-server -p codex-cloud-requirements -p codex-cloud-tasks -p
codex-models-manager -p codex-chatgpt -p codex-model-provider -p
codex-mcp -p codex-core-skills`
- `cargo test -p codex-model-provider bearer_auth_provider`
- `cargo test -p codex-core agent_assertion`
- `cargo test -p codex-app-server remote_control`
- `cargo test -p codex-cloud-requirements fetch_cloud_requirements`
- `cargo test -p codex-models-manager manager::tests`
- `cargo test -p codex-chatgpt`
- `cargo test -p codex-cloud-tasks`
- `cargo test -p codex-login agent_identity`
- `just fix -p codex-core -p codex-login -p codex-analytics -p
codex-app-server -p codex-cloud-requirements -p codex-cloud-tasks -p
codex-models-manager -p codex-chatgpt -p codex-model-provider -p
codex-mcp -p codex-core-skills`
- `just fix -p codex-app-server`
- `git diff --check`
This commit is contained in:
Adrian
2026-04-19 23:16:43 -07:00
committed by GitHub
Unverified
parent 3c75f9b4dd
commit b44d2851cf
19 changed files with 1206 additions and 65 deletions
@@ -101,6 +101,11 @@ impl Session {
agent_task
}
#[cfg(test)]
pub(crate) async fn cache_agent_task_for_tests(&self, agent_task: RegisteredAgentTask) {
self.cache_agent_task(agent_task).await;
}
pub(super) async fn cached_agent_task_for_current_identity(
&self,
) -> Option<RegisteredAgentTask> {
@@ -134,6 +139,33 @@ impl Session {
None
}
pub(crate) async fn authorization_header_for_current_agent_task(
&self,
) -> anyhow::Result<Option<String>> {
let Some(agent_task) = self.cached_agent_task_for_current_identity().await else {
return Ok(None);
};
let Some(auth) = self.services.auth_manager.auth().await else {
return Ok(None);
};
let authorization_header_value = self
.services
.auth_manager
.chatgpt_agent_task_authorization_header_for_auth(
&auth,
agent_task.authorization_target(),
)?;
if authorization_header_value.is_some() {
debug!(
agent_runtime_id = %agent_task.agent_runtime_id,
task_id = %agent_task.task_id,
"using agent assertion authorization for current task request"
);
}
Ok(authorization_header_value)
}
pub(super) async fn ensure_agent_task_registered(
&self,
) -> anyhow::Result<Option<RegisteredAgentTask>> {
+6 -5
View File
@@ -619,6 +619,11 @@ impl Session {
config.analytics_enabled,
)
});
let agent_identity_manager = Arc::new(AgentIdentityManager::new(
config.as_ref(),
Arc::clone(&auth_manager),
session_configuration.session_source.clone(),
));
let services = SessionServices {
// Initialize the MCP connection manager with an uninitialized
// instance. It will be replaced with one created via
@@ -641,11 +646,7 @@ impl Session {
hooks,
rollout: Mutex::new(rollout_recorder),
user_shell: Arc::new(default_shell),
agent_identity_manager: Arc::new(AgentIdentityManager::new(
config.as_ref(),
Arc::clone(&auth_manager),
session_configuration.session_source.clone(),
)),
agent_identity_manager: Arc::clone(&agent_identity_manager),
shell_snapshot_tx,
show_raw_agent_reasoning: config.show_raw_agent_reasoning,
exec_policy,
+32 -16
View File
@@ -333,20 +333,23 @@ pub(crate) async fn run_turn(
}))
.await;
}
if let Err(error) = sess.ensure_agent_task_registered().await {
warn!(error = %error, "agent task registration failed");
sess.send_event(
turn_context.as_ref(),
EventMsg::Error(ErrorEvent {
message: format!(
"Agent task registration failed. Please try again; Codex will attempt to register the task again on the next turn: {error}"
),
codex_error_info: Some(CodexErrorInfo::Other),
}),
)
.await;
return None;
}
let agent_task = match sess.ensure_agent_task_registered().await {
Ok(agent_task) => agent_task,
Err(error) => {
warn!(error = %error, "agent task registration failed");
sess.send_event(
turn_context.as_ref(),
EventMsg::Error(ErrorEvent {
message: format!(
"Agent task registration failed. Please try again; Codex will attempt to register the task again on the next turn: {error}"
),
codex_error_info: Some(CodexErrorInfo::Other),
}),
)
.await;
return None;
}
};
if !skill_items.is_empty() {
sess.record_conversation_items(&turn_context, &skill_items)
@@ -371,8 +374,21 @@ pub(crate) async fn run_turn(
// `ModelClientSession` is turn-scoped and caches WebSocket + sticky routing state, so we reuse
// one instance across retries within this turn.
let mut client_session =
prewarmed_client_session.unwrap_or_else(|| sess.services.model_client.new_session());
let mut prewarmed_client_session = prewarmed_client_session;
if agent_task.is_some()
&& let Some(prewarmed_client_session) = prewarmed_client_session.as_mut()
{
prewarmed_client_session.disable_cached_websocket_session_on_drop();
}
let mut client_session = if let Some(agent_task) = agent_task {
sess.services
.model_client
.new_session_with_agent_task(Some(agent_task))
} else if let Some(prewarmed_client_session) = prewarmed_client_session.take() {
prewarmed_client_session
} else {
sess.services.model_client.new_session()
};
// Pending input is drained into history before building the next model request.
// However, we defer that drain until after sampling in two cases:
// 1. At the start of a turn, so the fresh user prompt in `input` gets sampled first.