diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs index d58d1f878..938e36a8e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs @@ -1922,19 +1922,25 @@ impl CodexAppServerConnection { "创建 Codex app-server 临时目录失败:{error}" )) })?; - let direct_provider_route = match credential { + let (direct_provider_route, main_site_upstream) = match credential { CodexAppServerCredential::PlatformSession { api_base_url, access_token, .. - } => Some(( - format!("{}/api/llm", api_base_url.trim_end_matches('/')), - access_token.clone(), - )), - _ => (workspace_mode == CodexAppServerWorkspaceMode::DirectProject) - .then(|| credential.direct_provider_route(llm)) - .flatten() - .map(|(base_url, api_key)| (base_url.to_string(), api_key.to_string())), + } => ( + Some(( + format!("{}/api/llm", api_base_url.trim_end_matches('/')), + access_token.clone(), + )), + true, + ), + _ => ( + (workspace_mode == CodexAppServerWorkspaceMode::DirectProject) + .then(|| credential.direct_provider_route(llm)) + .flatten() + .map(|(base_url, api_key)| (base_url.to_string(), api_key.to_string())), + false, + ), }; let remote_control_disable_reason = credential.remote_control_disable_reason(direct_provider_route.is_some()); @@ -2024,7 +2030,7 @@ impl CodexAppServerConnection { }; let provider_proxy = if let Some((base_url, api_key)) = direct_provider_route.as_ref() { Some( - start_codex_provider_proxy(base_url, api_key) + start_codex_provider_proxy(base_url, api_key, main_site_upstream) .await .map_err(platform_llm::LlmError::InvalidConfig)?, ) @@ -4966,7 +4972,7 @@ mod tests { #[tokio::test] async fn direct_project_command_receives_only_provider_proxy_session_token() { let provider_key = "fixture-upstream-provider-secret"; - let proxy = start_codex_provider_proxy("http://127.0.0.1:9", provider_key) + let proxy = start_codex_provider_proxy("http://127.0.0.1:9", provider_key, false) .await .expect("start credential broker"); let mut command = tokio::process::Command::new("fixture"); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_provider_proxy.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_provider_proxy.rs index b23b50749..a38811148 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_provider_proxy.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_provider_proxy.rs @@ -9,12 +9,15 @@ use std::sync::Arc; pub(crate) const CODEX_PROVIDER_PROXY_PROTOCOL: &str = "genarrative-codex-provider-proxy.v1"; const CODEX_PROVIDER_PROXY_MAX_REQUEST_BYTES: usize = 32 * 1024 * 1024; +const AGC_CLIENT_MARKER_HEADER: &str = "x-genarrative-client"; +const AGC_CLIENT_MARKER_VALUE: &str = "agc"; #[derive(Clone)] struct CodexProviderProxyState { upstream_base_url: String, upstream_bearer_token: String, downstream_bearer_token: String, + main_site_upstream: bool, client: reqwest::Client, } @@ -137,10 +140,14 @@ async fn proxy_codex_provider_request( headers.append(name.clone(), value.clone()); } } - headers.insert( - axum::http::HeaderName::from_static("x-genarrative-client"), - axum::http::HeaderValue::from_static("agc"), - ); + // 仅 AGC 主站 `/api/llm` 路由需要携带保留的客户端标记,供服务端校验模型 + // 方案;通用 Provider/凭据桥接不得把该标记外发给第三方上游。 + if state.main_site_upstream { + headers.insert( + axum::http::HeaderName::from_static(AGC_CLIENT_MARKER_HEADER), + axum::http::HeaderValue::from_static(AGC_CLIENT_MARKER_VALUE), + ); + } let upstream_authorization = match format!("Bearer {}", state.upstream_bearer_token).parse() { Ok(value) => value, Err(_) => { @@ -193,6 +200,7 @@ async fn proxy_codex_provider_request( pub(crate) async fn start_codex_provider_proxy( upstream_base_url: &str, upstream_bearer_token: &str, + main_site_upstream: bool, ) -> Result { let upstream_base_url = normalize_codex_provider_upstream(upstream_base_url)?; let upstream_bearer_token = upstream_bearer_token.trim(); @@ -223,6 +231,7 @@ pub(crate) async fn start_codex_provider_proxy( upstream_base_url, upstream_bearer_token: upstream_bearer_token.to_string(), downstream_bearer_token: downstream_bearer_token.clone(), + main_site_upstream, client, }); let app = Router::new() @@ -269,6 +278,25 @@ mod tests { .expect("fake response") } + async fn fake_main_site_upstream( + State(calls): State>, + headers: HeaderMap, + body: axum::body::Bytes, + ) -> Response { + calls.fetch_add(1, Ordering::SeqCst); + assert_eq!( + headers + .get(AGC_CLIENT_MARKER_HEADER) + .and_then(|value| value.to_str().ok()), + Some(AGC_CLIENT_MARKER_VALUE) + ); + Response::builder() + .status(StatusCode::OK) + .header("content-type", "application/json") + .body(Body::from(body)) + .expect("fake main-site response") + } + #[tokio::test] async fn loopback_proxy_strips_false_codex_limit_headers_and_requires_bearer() { let calls = Arc::new(AtomicUsize::new(0)); @@ -285,6 +313,7 @@ mod tests { let proxy = start_codex_provider_proxy( &format!("http://127.0.0.1:{}", address.port()), "fixture-provider-key", + false, ) .await .expect("start provider proxy"); @@ -328,4 +357,37 @@ mod tests { assert_eq!(calls.load(Ordering::SeqCst), 1); upstream_task.abort(); } + + #[tokio::test] + async fn loopback_proxy_adds_main_site_marker_only_when_bridging_the_agc_route() { + let calls = Arc::new(AtomicUsize::new(0)); + let listener = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0)) + .await + .expect("bind upstream"); + let address = listener.local_addr().expect("upstream address"); + let app = Router::new() + .route("/responses", post(fake_main_site_upstream)) + .with_state(Arc::clone(&calls)); + let upstream_task = tokio::spawn(async move { + let _ = axum::serve(listener, app).await; + }); + let proxy = start_codex_provider_proxy( + &format!("http://127.0.0.1:{}", address.port()), + "fixture-provider-key", + true, + ) + .await + .expect("start main-site provider proxy"); + + let accepted = reqwest::Client::new() + .post(format!("{}/responses", proxy.base_url())) + .bearer_auth(proxy.downstream_bearer_token()) + .body("{\"input\":\"ok\"}") + .send() + .await + .expect("accepted main-site response"); + assert_eq!(accepted.status(), StatusCode::OK); + assert_eq!(calls.load(Ordering::SeqCst), 1); + upstream_task.abort(); + } }