拒绝原因留痕前统一脱敏

- log_denied_reason 在唯一出口调用 sanitize_diagnostic_message,透传的宿主错误文本不再把绝对路径写进日志
- 去重键改用脱敏后的文本,内存与 stderr 拷贝同样不带原文
- 新增 denied_reason_logging_scrubs_embedded_paths 用例固化该约定
- 同步更新 shared-memory 决策记录里的脱敏口径
This commit is contained in:
2026-10-01 15:48:25 +08:00
parent ce4ada8bb3
commit 493457c3ea
2 changed files with 18 additions and 1 deletions
@@ -389,6 +389,10 @@ impl ExecutionAdapter {
/// 同一回合内 `(method, reason)` 只记一次:模型被拒后常反复重试同一动作,逐次记录会把真正
/// 有用的诊断刷掉;"这一轮被拒过哪些原因"仍然完整。
fn log_denied_reason(&self, method: &str, reason: &str) {
// 契约:留痕不带路径、上游正文或凭据。静态分类本来就没有;`lease-admit-failed` 这类透传的
// 宿主错误文本可能内嵌绝对项目路径(如 `validate_patch_paths` 的路径校验错误),所以统一
// 在唯一出口脱敏——去重键用脱敏后的文本,日志拷贝与 stderr 拷贝都不会带原文。
let reason = crate::sanitize_diagnostic_message(reason, None);
let key = format!("{method}\u{1}{reason}");
let thread_id = &self.thread_id;
self.log_once(key, |distinct| {
@@ -1847,4 +1851,17 @@ mod tests {
MAX_DENIED_REASON_LOGS
);
}
#[test]
fn denied_reason_logging_scrubs_embedded_paths() {
let (_temp, adapter) = fixture();
adapter.log_denied_reason(
"item/commandExecution/requestApproval",
"lease-admit-failed: 读取路径失败:/home/someone/secret-project/app.ts",
);
let logged = adapter.denied_reasons_logged.lock().unwrap();
let key = logged.iter().next().expect("拒绝原因应留痕");
assert!(key.contains("lease-admit-failed"), "{key}");
assert!(!key.contains("/home/someone"), "留痕不得带绝对路径:{key}");
}
}
@@ -4,7 +4,7 @@
- 决策:宿主拒绝 app-server 的审批 / 交互请求时,原因必须落 AppData `RUST` 日志。稳定键 `agent.direct_codex.approval.denied`,字段为 `thread_id` / `method` / `reason` / `distinct_reasons`;未绑定宿主执行器时另记 `agent.direct_codex.interaction.no_adapter`(`method` / `outcome`,`outcome` 区分 `decline` / `empty-permissions` / `unsupported-method`);回包未送达另记 `agent.direct_codex.approval.response_delivery_failed`,`cause` 区分 `response-write-failed` 与 `turn-bind-mismatch`。
- 原因:线上对审批只回 `{"decision":"decline"}`,模型与用户都看不到是哪一道闸门(合同缺失 / 预算耗尽 / 阶段已收束 / item 不在途 / 无适配器)拦下的,只能靠复现。
- 边界:`reason` 只接受 `execution.rs` 内的静态分类或宿主自己的错误文本,不带请求参数、上游正文、路径或凭据;有界去重一律按 `MAX_DENIED_REASON_LOGS = 64` 条封顶——适配器路径按回合内 `(method, reason)`、未绑定执行器路径按进程级 `(method, outcome)`、回包未送达按回合内原因各只记一次。
- 边界:`reason` 只接受 `execution.rs` 内的静态分类或宿主自己的错误文本;透传的宿主错误在留痕出口统一走 `sanitize_diagnostic_message`,不带请求参数、上游正文、路径或凭据(去重键也用脱敏后的文本)。有界去重一律按 `MAX_DENIED_REASON_LOGS = 64` 条封顶——适配器路径按回合内 `(method, reason)`、未绑定执行器路径按进程级 `(method, outcome)`、回包未送达按回合内原因各只记一次。
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/execution.rs`。
- 验证:`cargo test --bin genarrative-ai-game-creator-shell agent::codex_app_server::execution::tests`(15 passed,含 `denied_approval_records_its_reason_once_per_turn`、`denied_reason_logging_is_bounded`)、`npm run check:encoding`、`git diff --check`。