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:
peilin-openai
2026-09-21 18:19:07 +00:00
committed by copyberry
parent 7cec3e110c
commit ac98537678
28 changed files with 1119 additions and 73 deletions
+36
View File
@@ -52,6 +52,42 @@ pub fn is_node_repl_backed_tool(name: &str, namespace: Option<&str>) -> bool {
.is_some_and(|(server, _)| is_node_repl_backed_server(server))
}
/// Producer-reported completeness of the thread's MCP tools/call attribution.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum McpAttributionStatus {
#[default]
None,
Complete,
AttributionError,
}
/// One source and the first runtime turn in which Codex recorded it.
///
/// Server and tool names are retained when no stable identifier is available;
/// they are not guaranteed to be globally unique or independently authenticated.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct McpAttributionSource {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub connector_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub plugin_id: Option<String>,
pub server_name: String,
pub tool_name: String,
pub first_turn_id: String,
}
/// Cumulative, model-invisible attribution reported on Responses API requests.
/// This is provenance data, not a training-eligibility decision or attestation.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct McpAttribution {
pub status: McpAttributionStatus,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub sources: Vec<McpAttributionSource>,
}
/// Bounded app-resource provenance retained across a compaction checkpoint.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct McpResourceOriginCheckpoint {