test(presenter): M103 r2 —— 出站脱敏补守卫,另三处站点与 Bearer 右边界

P1(守卫缺失,产品代码是对的):
- 新增 RecordingTransport + outbound_prompt_is_scrubbed_before_it_reaches_the_backend:
  断言真正发给后端的那份 prompt 里没有 JWT(rail 侧也脱敏,「界面干净」证明不了出站安全)
- 变异:删掉 agent_submit 的出站 scrub ⇒ 该测试红(输出见完成报告)

另三处同类站点(r2 P2-1):
- confirm_tool_name_is_scrubbed(ConfirmRequest.tool)
- tool_message_from_the_backend_has_its_tool_view_scrubbed(MessageAppended 的 ToolCallView 分支)
- outbound_error_text_is_scrubbed(Err 文案,provider 401 常回显 key)
- 三条各自变异后都变红

其他:
- bearer_right_boundary_is_pinned(tests.rs):钉住 mask_bearer 的停字符集,收窄成 [' '] 即红
- 文档订正:rail.rs 模块 §2 改成「两道闸门(出站/显示)各守一侧」+ 与 agent 侧的实际关系
  (名单逐字相同、path 语义相同、mask_bearer 是行为不完全等价的自写迁移;重复实现是欠账,
  不读成「已无差异」也不读成「已经漂移」)
- agent_set_draft 与 rail_text 各写明「draft 不在断言覆盖范围」及原因
- tests_red_line.rs 4 行 405 → 380;各文件均 ≤400
This commit is contained in:
AskaEth committed 2026-11-04 00:21:19 +08:00
1 parent 06461df91b
commit a2b1a06901
4 files changed
+222 -17

No files matched your search

+10 -2
View File
@@ -191,6 +191,10 @@ impl Presenter {
}
/// 输入框草稿(用户打字时同步进来,切形态不丢)。
///
/// 草稿**是** rail 上唯一能合法躺着凭据原文的字段:它是「还没发出去的字」,写进来时不过
/// 脱敏(UI 要能原样显示用户正在打的字)。安全性靠另一条:`agent_submit` 脱敏的是**入参**
/// 而不是草稿,所以原文既不会进会话历史,也不会被发给后端。
pub fn agent_set_draft(&mut self, text: String) {
self.state.ai.draft = text;
}
@@ -226,12 +230,16 @@ impl Presenter {
if self.state.ai.busy {
return Err("上一轮回复还没结束".to_string());
}
// 用户输入也过脱敏:他可能顺手把 token 粘进对话框,那段文本会进会话历史、
// 也会进后续 prompt 与导出。
// 用户输入也过脱敏:他可能顺手把 token 粘进对话框,而这段文本会进会话历史、
// 会作为 prompt **发到模型服务商**、也会进导出。这道是**出站**闸门(rail 侧那道是
// 显示闸门,两者不能互相替代:删掉这道,界面照样干净,但凭据已经出去了)。
// 守卫见 agent/tests_red_line.rs::outbound_prompt_is_scrubbed_before_it_reaches_the_backend。
let text = scrub_text(trimmed);
let events = self
.transport
.submit(&text, &self.state.ai)
// 失败原因同样可能回显 key(provider 的 401 常把 key 抄进 message),
// 这条 Err 会直接显示在 UI 上,守卫见 outbound_error_text_is_scrubbed。
.map_err(|error| scrub_text(&error))?;
self.state.ai.draft.clear();
self.state.ai.error = None;