From 28fa655c26815402652bc29242ac9e09c218300e Mon Sep 17 00:00:00 2001 From: Zyy Date: Thu, 22 Oct 2026 00:02:57 +0800 Subject: [PATCH] fix(openai): scope input turns and preserve executable paths Map input turn references with the same account identity as request metadata. Limit the workspace preset to metadata usernames and remote namespaces; preserve project names and executable paths to avoid path failures and same-parent workspace collisions. Co-authored-by: Codex --- .../openai/src/transport/correlation.rs | 10 ++++ .../tests/provider/contract/correlation.rs | 54 +++++++++++++++++-- docs/api.md | 12 +++-- .../settings/components/privacy/presets.ts | 6 +-- 4 files changed, 72 insertions(+), 10 deletions(-) diff --git a/backend/crates/providers/openai/src/transport/correlation.rs b/backend/crates/providers/openai/src/transport/correlation.rs index 97700c2fd..91612e802 100644 --- a/backend/crates/providers/openai/src/transport/correlation.rs +++ b/backend/crates/providers/openai/src/transport/correlation.rs @@ -205,6 +205,16 @@ pub(super) fn scope_request( let Some(installation_id) = installation_id else { return; }; + // 输入项的轮次引用与请求 metadata 共用账号域,保留 item/call ID 和业务元数据 + if let Some(Value::Array(input)) = body.get_mut("input") { + for item in input { + if let Some(Value::Object(metadata)) = + item.get_mut("internal_chat_message_metadata_passthrough") + { + map_fields(metadata, installation_id, &["turn_id"]); + } + } + } // 只处理官方自动 Git 上下文的关联字段,不扫描普通用户输入和工具输出 let Ok(targets) = locate_contexts(body, true, || Ok(())) else { return; diff --git a/backend/crates/providers/openai/tests/provider/contract/correlation.rs b/backend/crates/providers/openai/tests/provider/contract/correlation.rs index 3e3da3d4c..652654304 100644 --- a/backend/crates/providers/openai/tests/provider/contract/correlation.rs +++ b/backend/crates/providers/openai/tests/provider/contract/correlation.rs @@ -16,11 +16,25 @@ fn payload(websocket: bool) -> ProtocolPayload { .as_array() .unwrap() .clone(); + for (index, item) in input.iter_mut().enumerate() { + let metadata = item + .as_object_mut() + .unwrap() + .entry("internal_chat_message_metadata_passthrough") + .or_insert_with(|| json!({})); + metadata["turn_id"] = json!(if index == 3 { + "source-turn" + } else { + "source-previous-turn" + }); + } input.insert(0, json!({"role":"user", "content":[{"type":"input_text", "text":concat!( "", r#"{"sessionId":"source-session","threadId":"source-thread","clientUserMessageId":"source-turn","path":"/workspace/project"}"#, "" - )}], "internal_chat_message_metadata_passthrough":{"content_item_kinds":["additional_content.codex_git_state"]}})); + )}], "internal_chat_message_metadata_passthrough":{ + "turn_id":"source-turn", "content_item_kinds":["additional_content.codex_git_state"] + }})); ProtocolPayload::json_object("openai", json!({ "model":"gpt-5.4", "input":input, "prompt_cache_key":"custom-cache-key", "client_metadata":{ @@ -143,7 +157,32 @@ async fn account_correlation_is_stable_across_transports_and_changes_only_with_a assert_eq!(full["tool_namespaces_info"]["inventory"], "body-only"); assert_eq!(full["future"], metadata()["future"]); let input = body["input"].as_array().unwrap(); - assert_eq!(json!(&input[1..]), account_isolation::isolation_history()); + assert_eq!( + input[0]["internal_chat_message_metadata_passthrough"], + json!({"turn_id":flat["turn_id"], "content_item_kinds":["additional_content.codex_git_state"]}), + ); + let previous_turn = &input[1]["internal_chat_message_metadata_passthrough"]["turn_id"]; + assert_scoped_id(previous_turn.as_str().unwrap(), "source-previous-turn"); + assert_ne!(previous_turn, &flat["turn_id"]); + let mut expected_history = account_isolation::isolation_history(); + for (index, item) in expected_history + .as_array_mut() + .unwrap() + .iter_mut() + .enumerate() + { + let metadata = item + .as_object_mut() + .unwrap() + .entry("internal_chat_message_metadata_passthrough") + .or_insert_with(|| json!({})); + metadata["turn_id"] = if index == 3 { + flat["turn_id"].clone() + } else { + previous_turn.clone() + }; + } + assert_eq!(json!(&input[1..]), expected_history); let document = roxmltree::Document::parse(input[0]["content"][0]["text"].as_str().unwrap()) .unwrap(); @@ -168,7 +207,15 @@ async fn account_correlation_is_stable_across_transports_and_changes_only_with_a body["prompt_cache_key"].as_str().unwrap(), "custom-cache-key", ); - captures.push((flat["session_id"].clone(), body["prompt_cache_key"].clone())); + captures.push(( + [ + flat["session_id"].clone(), + flat["thread_id"].clone(), + flat["turn_id"].clone(), + ], + body["prompt_cache_key"].clone(), + previous_turn.clone(), + )); } } for index in [1, 2, 3, 6, 7] { @@ -177,6 +224,7 @@ async fn account_correlation_is_stable_across_transports_and_changes_only_with_a assert_eq!(captures[4], captures[5]); assert_ne!(captures[0].0, captures[4].0); assert_ne!(captures[0].1, captures[4].1); + assert_ne!(captures[0].2, captures[4].2); } #[tokio::test] diff --git a/docs/api.md b/docs/api.md index 1b5711dac..7c26d41cd 100644 --- a/docs/api.md +++ b/docs/api.md @@ -299,6 +299,8 @@ Responses 也不透传 `x-stainless-*`、`Origin`、`Referer`、`sec-ch-ua*` 和 会话、线程、轮次、窗口及父子引用在出站时按账号安装身份稳定映射为 UUID; 同账号保留引用关系,换账号使用不同标识。HTTP 请求头、正文 metadata、WS 副本及 已识别的 Desktop 自动 Git 上下文使用一致映射。 +输入项的 `internal_chat_message_metadata_passthrough.turn_id` 同步映射, +保留历史轮次与当前轮次的引用关系,其余输入项元数据保持不变。 Images 的 `x-codex-image-turn-id`、turn metadata 和 Search 的会话 `id` 使用同一账号域; Search 只替换该 `id` 的原始字节,其余正文保持不变。 `prompt_cache_key` 同样按账号映射,保留同账号内的缓存共享关系;未提供时不补造。 @@ -1571,9 +1573,13 @@ JSON 路径支持 `$`、`.成员`、`["带特殊字符的键"]`、`[索引]`、` `$.workspaces.*.associated_remote_urls.*` 配合 `remove_field` 和 `pattern: "private-org"` 只删除命中的远端; `$.workspaces` 配合 `rename_key` 可改写工作区路径键名。环境文本替换必须保留 XML 包裹结构 -管理端提供「统一工作区信息」预设,生成一条含多个通用步骤的规则。 -预设统一路径中的用户名为 `user`,工作区路径键和环境工作目录保留实际目录名并改为 `/workspace/<项目名>`, -保留实际仓库、提交、分支、工作区标识和会话关联信息。安装身份由账号隔离逻辑处理, +管理端提供「统一工作区信息」预设,生成一条含多个步骤的规则,仅处理 turn metadata 的 `workspaces`: + +- `/home/<用户名>`、`/Users/<用户名>` 及 Windows 用户目录中的用户名改为 `user`,保留其余目录结构和项目名 +- Git 远端 URL 的账号或组织路径段改为 `alex`,保留协议、主机和仓库名,支持 HTTP(S)、SSH、Git 协议和 `git@host:owner/repo` 形式;多级命名空间及本地远端路径保持原值 + +提交、分支、工作区标识和会话关联信息保持原值。正文中的工作目录及其他执行路径也保持原值, +供客户端工具使用,仍可能包含真实用户名和项目名。安装身份由账号隔离逻辑处理, UA、平台参数及自动 Git 上下文版本由 [OpenAI 上游客户端身份](#openai-上游客户端身份)管理。 预设和自定义规则使用同一编辑器,步骤均可增删、修改和排序;调整预设不覆盖已保存的规则 diff --git a/frontend/src/views/settings/components/privacy/presets.ts b/frontend/src/views/settings/components/privacy/presets.ts index 8a55e545d..62e84afce 100644 --- a/frontend/src/views/settings/components/privacy/presets.ts +++ b/frontend/src/views/settings/components/privacy/presets.ts @@ -43,11 +43,9 @@ export const presets: { key: string, name: string, steps: Partial[] key: 'workspace', name: '统一工作区信息', steps: [ + // 只改写附加元数据中的路径,正文路径供本地工具执行使用 { scope: 'turn_metadata', selector: '$.workspaces', action: 'rename_key', pattern: String.raw`(?i)^((?:[a-z]:)?[/\\](?:home|users)[/\\])[^/\\]+`, replacement: `\${1}user` }, - { scope: 'environment_text', selector: '$', action: 'regex_replace', pattern: String.raw`(?i)((?:[a-z]:)?[/\\](?:home|users)[/\\])[^/\\<>"'\r\n]+`, replacement: `\${1}user` }, - // 路径隐藏本机目录布局,项目名和 Git 状态仍取自实际请求 - { scope: 'turn_metadata', selector: '$.workspaces', action: 'rename_key', pattern: String.raw`(?s)^(?:[a-zA-Z]:)?[/\\](?:.*[/\\])?([^/\\]+)[/\\]*$`, replacement: `/workspace/\${1}` }, - { scope: 'environment_text', selector: '$', action: 'regex_replace', pattern: String.raw`(\s*)(?:[a-zA-Z]:)?[/\\](?:[^<>\r\n]*[/\\])?([^/\\<>\r\n]+?)[/\\]*\s*()`, replacement: `\${1}/workspace/\${2}\${3}` }, + { scope: 'turn_metadata', selector: '$.workspaces.*.associated_remote_urls.*', action: 'regex_replace', pattern: String.raw`(?i)^((?:(?:https?|ssh|git)://[^/?#]+/|[^/@:\s]+@[^/:\s]+:))[^/?#]+/([^/?#]+)(/?)$`, replacement: `\${1}alex/\${2}\${3}` }, ], }, ]