统一 DirectProject 流式增量脱敏

对 reasoning 与 agentMessage 增量复用历史正文脱敏和截断规则

补充密钥与绝对路径不泄露的 Rust 回归测试
This commit is contained in:
2026-09-18 00:29:03 +08:00
parent de7211e8f2
commit 5a0a3362fb
2 changed files with 49 additions and 4 deletions
@@ -1124,6 +1124,16 @@ fn direct_codex_reasoning_delta_event(
}) })
} }
/// DirectProject 的流式正文在进入 Thread Manager 前就完成脱敏;前端不得接触原始增量。
fn direct_codex_thread_delta_event(
root: &std::path::Path,
item_id: String,
kind: DirectThreadDeltaKind,
delta: &str,
) -> DirectThreadEvent {
DirectThreadEvent::item_delta(item_id, kind, direct_thread_delta_text(root, delta))
}
/// 通知 → 回合事件的唯一分类函数:运行态读取器与单测共用这一份。 /// 通知 → 回合事件的唯一分类函数:运行态读取器与单测共用这一份。
/// ///
/// 读取器只负责"必须有 turnId 才处理"的前置条件与节流(活动 / 正文),分类不在这里之外 /// 读取器只负责"必须有 turnId 才处理"的前置条件与节流(活动 / 正文),分类不在这里之外
@@ -3105,10 +3115,11 @@ impl CodexAppServerConnection {
&direct_thread_id, &direct_thread_id,
// 事件自足:增量自带 item 身份与正文类别(正文 / 思考), // 事件自足:增量自带 item 身份与正文类别(正文 / 思考),
// 前端 reducer 不允许靠猜 itemId 的来源决定 kind。 // 前端 reducer 不允许靠猜 itemId 的来源决定 kind。
DirectThreadEvent::item_delta( direct_codex_thread_delta_event(
history_root,
item_id.clone(), item_id.clone(),
DirectThreadDeltaKind::Message, DirectThreadDeltaKind::Message,
delta.clone(), &delta,
), ),
); );
} }
@@ -3144,10 +3155,11 @@ impl CodexAppServerConnection {
if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject { if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject {
append_direct_thread_event( append_direct_thread_event(
&direct_thread_id, &direct_thread_id,
DirectThreadEvent::item_delta( direct_codex_thread_delta_event(
history_root,
item_id, item_id,
DirectThreadDeltaKind::Reasoning, DirectThreadDeltaKind::Reasoning,
delta, &delta,
), ),
); );
} }
@@ -4893,6 +4905,31 @@ mod tests {
)); ));
} }
#[test]
fn direct_thread_message_and_reasoning_deltas_are_sanitized_before_enqueue() {
let root = std::path::Path::new("/workspace/direct-project");
let raw = "key=sk-abcdefghijklmnop project=/workspace/direct-project/assets/a.png private=/root/secret.txt";
for kind in [
DirectThreadDeltaKind::Message,
DirectThreadDeltaKind::Reasoning,
] {
let event = direct_codex_thread_delta_event(root, "item-1".to_string(), kind, raw);
let wire = serde_json::to_string(&event).expect("serialize direct thread delta");
assert!(
!wire.contains("sk-abcdefghijklmnop"),
"不得泄漏密钥:{wire}"
);
assert!(
!wire.contains("/root/secret.txt"),
"不得泄漏绝对路径:{wire}"
);
assert!(
wire.contains("assets/a.png"),
"项目内绝对路径应归一成相对路径:{wire}"
);
}
}
/// 判据:读取器与单测共用同一个分类函数,这些分支的行为被钉在这里。 /// 判据:读取器与单测共用同一个分类函数,这些分支的行为被钉在这里。
/// ///
/// 参数:method / params / 正文候选 / 安全活动类别 / turnId。 /// 参数:method / params / 正文候选 / 安全活动类别 / turnId。
@@ -365,6 +365,14 @@ fn item_text(root: &Path, item: &Value) -> Option<String> {
)) ))
} }
/// 流式正文与完成态条目使用同一套脱敏和字符预算,避免增量文本绕过历史投影的安全边界。
pub(crate) fn direct_thread_delta_text(root: &Path, value: &str) -> String {
bounded(
&sanitize_detail_text(root, value),
DIRECT_THREAD_TEXT_MAX_CHARS,
)
}
fn item_at_ms(item: &Value, observed_at_ms: u64) -> u64 { fn item_at_ms(item: &Value, observed_at_ms: u64) -> u64 {
let from_metadata = item let from_metadata = item
.get("internal_chat_message_metadata_passthrough") .get("internal_chat_message_metadata_passthrough")