mirror of
https://github.com/openai/codex.git
synced 2026-11-07 20:30:33 +00:00
Track cumulative MCP attribution across requests and thread history (#47081)
## What changed - Record each MCP source and its first turn, including connector and plugin identifiers when available. Cover direct and Code Mode calls, including returned errors and results that are not printed. - Persist cumulative attribution checkpoints and restore them across compaction, resume, and forks, including forks limited to recent turns. - Attach fresh attribution to Responses API `client_metadata` under `mcp_attribution`, outside model-visible content. Restrict transmission to allowed OpenAI HTTPS destinations for both HTTP and WebSocket requests. - Report `attribution_error` for legacy history without checkpoints, malformed or conflicting attribution, and serialized attribution exceeding 16 KiB. ## Testing Add coverage for source deduplication, checkpoint persistence and restoration, recent-turn forks, direct and Code Mode calls, destination filtering, and the serialized size limit. GitOrigin-RevId: fd285e152ca9e24bfa000debb20013709d67a86e
This commit is contained in:
+30
-33
@@ -405,6 +405,16 @@ fn response_items_equal_ignoring_internal_metadata(
|
||||
previous == current
|
||||
}
|
||||
|
||||
/// Whether the resolved outbound Responses destination may receive internal tool metadata.
|
||||
fn is_internal_metadata_destination(provider: &ApiProvider) -> bool {
|
||||
url::Url::parse(&provider.base_url).ok().is_some_and(|url| {
|
||||
url.scheme() == "https"
|
||||
&& url.host_str().is_some_and(|host| {
|
||||
host == "api.openai.com" || codex_http_client::is_allowed_chatgpt_host(host)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
impl WebsocketSession {
|
||||
fn reset(&mut self, reason: Option<&'static str>) {
|
||||
// Per-socket backend metrics call a resend after reconnect "initial".
|
||||
@@ -802,9 +812,10 @@ impl ModelClient {
|
||||
fn build_ws_client_metadata(
|
||||
&self,
|
||||
responses_metadata: &CodexResponsesMetadata,
|
||||
include_internal: bool,
|
||||
use_responses_lite: bool,
|
||||
) -> HashMap<String, String> {
|
||||
let mut client_metadata = responses_metadata.client_metadata();
|
||||
let mut client_metadata = responses_metadata.client_metadata(include_internal);
|
||||
if use_responses_lite {
|
||||
client_metadata.insert(
|
||||
WS_REQUEST_HEADER_RESPONSES_LITE_CLIENT_METADATA_KEY.to_string(),
|
||||
@@ -866,6 +877,7 @@ impl ModelClient {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn build_responses_request(
|
||||
&self,
|
||||
prompt: &Prompt,
|
||||
@@ -874,6 +886,7 @@ impl ModelClient {
|
||||
summary: ReasoningSummaryConfig,
|
||||
service_tier: Option<String>,
|
||||
responses_metadata: &CodexResponsesMetadata,
|
||||
include_internal: bool,
|
||||
) -> Result<ResponsesApiRequest> {
|
||||
let mut input = prompt.get_formatted_input_for_request(model_info);
|
||||
if !self.reasoning_effort_override_enabled(model_info) {
|
||||
@@ -963,6 +976,12 @@ impl ModelClient {
|
||||
} else {
|
||||
model_info.service_tier_for_request(service_tier)
|
||||
};
|
||||
if !include_internal {
|
||||
for item in &mut input {
|
||||
item.clear_tool_result_metadata();
|
||||
}
|
||||
}
|
||||
let client_metadata = responses_metadata.client_metadata(include_internal);
|
||||
let request = ResponsesApiRequest {
|
||||
model: model_info.slug.clone(),
|
||||
instructions,
|
||||
@@ -978,32 +997,12 @@ impl ModelClient {
|
||||
service_tier,
|
||||
prompt_cache_key,
|
||||
text,
|
||||
client_metadata: Some(responses_metadata.client_metadata()),
|
||||
client_metadata: Some(client_metadata),
|
||||
access_programs: None,
|
||||
};
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
fn filter_tool_result_metadata(input: &mut [ResponseItem], api_provider: &ApiProvider) {
|
||||
// Check the resolved destination only when sending, not for local budget estimates.
|
||||
// HTTP and WS (including v2 compaction) share this raw-metadata-only filter.
|
||||
let result_metadata_allowed =
|
||||
url::Url::parse(&api_provider.base_url)
|
||||
.ok()
|
||||
.is_some_and(|url| {
|
||||
url.scheme() == "https"
|
||||
&& url.host_str().is_some_and(|host| {
|
||||
host == "api.openai.com"
|
||||
|| codex_http_client::is_allowed_chatgpt_host(host)
|
||||
})
|
||||
});
|
||||
if !result_metadata_allowed {
|
||||
for item in input {
|
||||
item.clear_tool_result_metadata();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_response_items_for_request(&self, input: &mut [ResponseItem]) {
|
||||
for item in input {
|
||||
if item.id().is_some_and(|id| !id.is_prefixed()) {
|
||||
@@ -1629,6 +1628,7 @@ impl ModelClientSession {
|
||||
.client
|
||||
.current_client_setup(ClientRouting::Workspace)
|
||||
.await?;
|
||||
let include_internal = is_internal_metadata_destination(&client_setup.api_provider);
|
||||
let responses_headers = self
|
||||
.client
|
||||
.responses_headers(client_setup.auth.as_ref(), &model_info.slug);
|
||||
@@ -1666,11 +1666,8 @@ impl ModelClientSession {
|
||||
summary,
|
||||
service_tier.clone(),
|
||||
responses_metadata,
|
||||
include_internal,
|
||||
)?;
|
||||
ModelClient::filter_tool_result_metadata(
|
||||
&mut request.input,
|
||||
&client_setup.api_provider,
|
||||
);
|
||||
self.client.set_guardian_metadata(
|
||||
&mut request.client_metadata,
|
||||
responses_metadata.parent_response_id.as_deref(),
|
||||
@@ -1811,6 +1808,7 @@ impl ModelClientSession {
|
||||
.client
|
||||
.current_client_setup(ClientRouting::Workspace)
|
||||
.await?;
|
||||
let include_internal = is_internal_metadata_destination(&client_setup.api_provider);
|
||||
let responses_headers = self
|
||||
.client
|
||||
.responses_headers(client_setup.auth.as_ref(), &model_info.slug);
|
||||
@@ -1828,11 +1826,8 @@ impl ModelClientSession {
|
||||
summary,
|
||||
service_tier.clone(),
|
||||
responses_metadata,
|
||||
include_internal,
|
||||
)?;
|
||||
ModelClient::filter_tool_result_metadata(
|
||||
&mut request.input,
|
||||
&client_setup.api_provider,
|
||||
);
|
||||
let guardian_reviewer = responses_headers
|
||||
.get("x-codex-guardian")
|
||||
.is_some_and(|value| value == "reviewer");
|
||||
@@ -1905,9 +1900,11 @@ impl ModelClientSession {
|
||||
{
|
||||
crate::guardian::observe_guardian_request(session_telemetry, &request);
|
||||
}
|
||||
let mut client_metadata = self
|
||||
.client
|
||||
.build_ws_client_metadata(responses_metadata, model_info.use_responses_lite);
|
||||
let mut client_metadata = self.client.build_ws_client_metadata(
|
||||
responses_metadata,
|
||||
include_internal,
|
||||
model_info.use_responses_lite,
|
||||
);
|
||||
if let Some(turn_state) = self.turn_state.get() {
|
||||
client_metadata.insert(X_CODEX_TURN_STATE_HEADER.to_string(), turn_state.clone());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user