Enable git attribution across Codex entry points (#34819)

## What changed

- Install the git attribution extension in the app server, MCP server, and `codex debug prompt-input` so authenticated workspace policy controls the commit and pull request attribution instructions sent to the model.
- Resolve attribution settings from the process-level ChatGPT base URL, independent of per-thread or per-tool configuration overrides.
- Treat git attribution as contextual developer content when mapping model events.

## Testing

- Cover policy fetch retries, workspace switches, rollbacks, cold thread resumes with legacy instructions, MCP tool calls, and prompt-debug output.

GitOrigin-RevId: 57d182c432c20d7c1c6c429057b6163c9f32088d
This commit is contained in:
jay authored and copyberry committed 2026-07-22 20:23:35 +00:00
1 parent 4ebd976312
commit 88eb3a2b8a
21 files changed
+600 -166

No files matched your search

+52 -3
View File
@@ -3,6 +3,9 @@ use std::env;
use std::path::Path;
use std::path::PathBuf;
use app_test_support::ChatGptAuthFixture;
use app_test_support::write_chatgpt_auth;
use codex_config::types::AuthCredentialsStoreMode;
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
use codex_mcp_server::CodexToolCallParam;
use codex_mcp_server::ExecApprovalElicitRequestParams;
@@ -19,7 +22,11 @@ use rmcp::model::RequestId;
use serde_json::json;
use tempfile::TempDir;
use tokio::time::timeout;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::method;
use wiremock::matchers::path;
use core_test_support::skip_if_no_network;
use mcp_test_support::McpProcess;
@@ -366,17 +373,39 @@ async fn codex_tool_passes_base_instructions() -> anyhow::Result<()> {
let server =
create_mock_responses_server(vec![create_final_assistant_message_sse_response("Enjoy!")?])
.await;
let caller_server = MockServer::start().await;
// Run `codex mcp` with a specific config.toml.
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
let mut mcp_process = McpProcess::new(codex_home.path()).await?;
write_chatgpt_auth(
codex_home.path(),
ChatGptAuthFixture::new("chatgpt-token").account_id("workspace-123"),
AuthCredentialsStoreMode::File,
)?;
Mock::given(method("GET"))
.and(path("/backend-api/wham/settings/user"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"commit_attribution_enabled": true,
})))
.expect(1)
.mount(&server)
.await;
let mut mcp_process = McpProcess::new_with_env(
codex_home.path(),
&[("OPENAI_API_KEY", None), ("CODEX_ACCESS_TOKEN", None)],
)
.await?;
timeout(DEFAULT_READ_TIMEOUT, mcp_process.initialize()).await??;
// Send a "codex" tool request, which should hit the responses endpoint.
let codex_request_id = mcp_process
.send_codex_tool_call(CodexToolCallParam {
prompt: "How are you?".to_string(),
config: Some(HashMap::from([(
"chatgpt_base_url".to_string(),
json!(format!("{}/backend-api", caller_server.uri())),
)])),
base_instructions: Some("You are a helpful assistant.".to_string()),
developer_instructions: Some("Foreshadow upcoming tool calls.".to_string()),
..Default::default()
@@ -412,12 +441,15 @@ async fn codex_tool_passes_base_instructions() -> anyhow::Result<()> {
);
let requests = server.received_requests().await.unwrap();
let request = requests[0].body_json::<serde_json::Value>()?;
let request = requests
.iter()
.find(|request| request.url.path() == "/v1/responses")
.expect("mock model request should be recorded")
.body_json::<serde_json::Value>()?;
let instructions = request["instructions"]
.as_str()
.expect("responses request should include instructions");
assert!(instructions.starts_with("You are a helpful assistant."));
let developer_messages: Vec<&serde_json::Value> = request["input"]
.as_array()
.expect("responses request should include input items")
@@ -431,6 +463,14 @@ async fn codex_tool_passes_base_instructions() -> anyhow::Result<()> {
.filter(|span| span.get("type").and_then(serde_json::Value::as_str) == Some("input_text"))
.filter_map(|span| span.get("text").and_then(serde_json::Value::as_str))
.collect();
let developer_text = developer_contents.join("\n");
assert_eq!(
developer_text
.matches("Co-authored-by: Codex <noreply@openai.com>")
.count(),
1
);
assert_eq!(developer_text.matches("Generated with Codex.").count(), 1);
assert!(
developer_contents
.iter()
@@ -441,6 +481,13 @@ async fn codex_tool_passes_base_instructions() -> anyhow::Result<()> {
developer_contents.contains(&"Foreshadow upcoming tool calls."),
"expected developer instructions in developer messages, got {developer_contents:?}"
);
let caller_requests = caller_server.received_requests().await.unwrap();
assert!(
caller_requests
.iter()
.all(|request| request.url.path() != "/backend-api/wham/settings/user"),
"attribution settings must use the process-level base URL"
);
Ok(())
}
@@ -513,6 +560,8 @@ approval_policy = "untrusted"
sandbox_policy = "workspace-write"
model_provider = "mock_provider"
chatgpt_base_url = "{server_uri}/backend-api"
cli_auth_credentials_store = "file"
[model_providers.mock_provider]
name = "Mock provider for test"