统一错误事件同步留痕到 AppData 应用日志 #453

Merged
k88936 merged 8 commits from feat/log-the-dianosis into master 2026-09-22 10:54:42 +08:00
Member

统一错误事件的同一份已脱敏诊断投影成应用日志

统一错误事件的同一份已脱敏诊断投影成应用日志
k88936 added 1 commit 2026-09-21 22:43:12 +08:00
统一错误事件同步留痕到 AppData 应用日志
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m57s
Project CI / Backend tests (pull_request) Failing after 12s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 3m50s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 6m44s
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 7m28s
Project CI / Frontend tests (pull_request) Successful in 4m9s
Project CI / Native shell tests (pull_request) Successful in 8m24s
Project CI / AI game creator shell web tests (pull_request) Successful in 5m37s
8b2d0cb9df
- 新增 agent_runtime_error_app_log_lines,把统一错误事件的同一份已脱敏诊断投影成应用日志两行:agent.runtime.error 身份行与 agent.runtime.error.detail 详情行
- 身份行含 eventId/source/stage/code/retryable/clientTurnId/elapsedMs/detailRef/summary,详情行含 hint/detail/metadata;detail 与 metadata 各按 1200/200 字符预算先脱敏再截断,字段仍只来自 sidecar 那份 diagnosis
- 拆两行的原因:整行命中凭据标记会被 sanitize_diagnostic_message 整体替换成脱敏占位,详情行被吃掉时身份行仍能定位 eventId 与 detailRef
- app_log! 先于 sidecar 写入,sidecar 写失败也留下可提交的诊断;各字段先压平换行,保证应用日志逐行读取且 stderr 输出不拆行
- 新增用例覆盖身份字段、凭据脱敏、单行口径与实际 sanitize_diagnostic_message 落盘边界(含超长诊断)
- 同步文件【技术方案】AGC错误报告与诊断上传与【技术方案】AI游戏创作智能体App实施计划,共享记忆 decision-log 与 pitfalls 明确项目内 .agent/runtime/errors sidecar 与进程内错误报告事件池是两套东西,本次只写日志行
- 验证:cargo test runtime_error(7 passed)、cargo fmt --check、npm run check:encoding、git diff --check
k88936 added 7 commits 2026-09-22 10:27:07 +08:00
- 身份行的 summary 由调用方给,direct_tool_bridge 传的是工具错误原文,而 app_log! 会把整行写 stderr,那里没有 sanitize_diagnostic_message 兜底
- 新增 AGENT_RUNTIME_ERROR_APP_LOG_SUMMARY_CHARS(320 字符,与 sidecar 摘要预算同口径),summary 落日志前先脱敏再截断
- 函数合同写明 detail/metadata/summary 三类外来文本都在这里脱敏,不再只是压平换行
- 同步【技术方案】AGC错误报告与诊断上传与共享记忆 decision-log 的预算口径
- 验证:cargo test runtime_error(7 passed)
- metadata 之前直接以 {metadata} 插值进日志行,只靠 Value::to_string() 的转义保证单行,和函数「每个字段都先压平」的约定不一致
- 改为与其他字段一样走 single_line_log_field,日志行完整性不再依赖序列化器的转义行为
- 验证:cargo test runtime_error(7 passed)
- 生产是把两行分别交给 append_application_log_line 脱敏截断,原用例先 join(" ") 再 sanitize,测不到逐行截断与「只该脱敏其中一行」的情况
- 改为逐行 sanitize,并分别断言身份行(eventId/code/detailRef/summary)与详情行(detail 脱敏、metadata 无凭据)
- 验证:cargo test runtime_error(7 passed)
- 原注释写「只再按应用日志预算截一次」,实现却是完整脱敏流水线,合同与实现不一致
- 采纳方式是把合同写实:detail/metadata/summary 都在这里脱敏;已脱敏的 detail 也走同一遍,理由是截断会切开脱敏标记,且这是 pub(crate) 边界不假设调用方先脱敏
- 未采纳「去掉再脱敏改为只截断」:该建议会把安全性绑定在调用方自觉上,收益只是一次可忽略的 canonicalize
- 验证:rustfmt(注释改动,无行为变化)
- 原用例直接传带 token=secret 的原文,与函数「detail 已是 sidecar 脱敏文本」的前置条件不符,测的不是真实数据流
- 改为先按 8 KiB 口径 redact 出 safe_detail 再传入,metadata 仍按生产原样传未脱敏 JSON,保留脱敏覆盖
- 验证:cargo test runtime_error(7 passed)
- 身份行只留程序生成或调用方常量字段(eventId / source / stage / code / retryable / clientTurnId / elapsedMs / detailRef),去掉了 summary
- summary 移到详情行(hint / summary / detail / metadata),自由文本全部只出现在详情行
- 原因:sanitize_diagnostic_message 命中凭据标记时替换整行,且裸词标记(如 credential rotation failed)脱敏消不掉,summary 留在身份行会连 eventId、detailRef 一起被替换
- 用例补一条裸标记词场景:详情行被整体替换时身份行仍能按 eventId 与 code 定位
- 同步【技术方案】AGC错误报告与诊断上传、【技术方案】AI游戏创作智能体App实施计划、决策记录与踩坑记录的两行字段口径
- 验证:cargo test -- runtime_error direct_tool_bridge(41 passed)、cargo fmt --check、git diff --check
Merge remote-tracking branch 'refs/remotes/origin/master' into feat/log-the-dianosis
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m56s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 3m49s
Project CI / Backend tests (pull_request) Successful in 5m7s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m13s
Project CI / Native shell tests (pull_request) Successful in 6m34s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m57s
Project CI / Repository checks (pull_request) Successful in 2m25s
Project CI / Frontend tests (pull_request) Successful in 3m39s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m46s
a3586bc995
# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
#	docs/project-memory/shared-memory/pitfalls.md
Author
Member
  • 1 身份行 summary 只压行未脱敏(security · medium)

    • 现状(修复前):agent_runtime_error_app_log_linespublic_text 只做 single_line_log_field;身份行 summary= 原样进 app_log!。现在该行:runtime_error.rs:173。
    • 问题成立:app_log!(main.rs:73)先 format!eprintln!,stderr 这条路径没有 sanitize_diagnostic_message;而 direct_tool_bridge.rs:3348 把工具错误原文 result.pointer("/content/0/text") 同时当 public_textdetail 传进来,内容不可控。落盘那份虽有兜底,但 stderr 会漏原文。
    • 修复:新增 AGENT_RUNTIME_ERROR_APP_LOG_SUMMARY_CHARS = 320(与 sidecar 摘要预算同口径),summary 先 redact_agent_runtime_error 再压行;函数合同改为「summary / detail / metadata 三类外来文本都在这里脱敏」,不再只是压平换行。同步了两份技术文档与 decision-log 的预算口径。
    • 未一并处理:调用侧(第 7 条)仍把原文当 public_text,属用户可见文本,留给你。
  • 2 metadata 没走压行(maintainability · low)

    • 现状(修复前):详情行直接 metadata={metadata} 插值,只靠 Value::to_string() 的 JSON 转义保证单行。现在该行:runtime_error.rs:190。
    • 问题成立:与函数「每个字段都先压平」的约定不一致,把日志行完整性绑在序列化器的转义行为上(今天安全,但契约上说不通)。
    • 修复:metadata 与其他字段一样过 single_line_log_field
  • 3 用例先 join 再 sanitize,测不到逐行行为(test · low)

    • 现状(修复前):用例把两行 join(" ") 后只 sanitize 一次。现在该用例:runtime_error.rs:262。
    • 问题成立:生产是 app_log!append_application_log_line 逐行脱敏与逐行截断(chars().take(2_048)),拼接后测不到「只该脱敏其中一行」「身份行被截断」这两类。
    • 修复:改为逐行 sanitize,再分别断言身份行(eventId / code / detailRef / summary)与详情行(detail 的路径与 URL 已脱敏、metadata 无凭据),长度上限仍逐行核。
  • 5 「只再截一次」的注释与实现不符(maintainability · low)

    • 现状(修复前):注释写「传进来的 detail 已经是脱敏文本,这里只再按预算截一次」,实现却对 detail 跑完整 redact_agent_runtime_error(含 root.canonicalize() 与标记保留逻辑)。现在注释:runtime_error.rs:137。
    • 症状成立:合同与实现确实不一致。
    • 处理:未采纳「去掉再脱敏、只做截断」的建议,改为把合同写实——summary / detail / metadata 都在这里过一遍脱敏,已脱敏的 detail 也照走。理由:① 截断会把 [redacted-secret] 这类标记切开,不能假设截断后的文本仍满足前置条件;② 这是 pub(crate) 边界,不应把安全性绑定在调用方自觉上;③ 代价只是一次可忽略的 canonicalize(只在终态失败时发生)。
    • 若你更看重那点 I/O 与「单一职责」,可以把 detail 的分支换成纯截断,但那要先接受上面两条前提。
  • 6 用例传的是未脱敏原文,与前置条件不符(test · low)

    • 现状(修复前):用例直接传含 token=secret 的原文 detail,而生产 persist_agent_runtime_error 传的是已脱敏的 safe_detail。现在该用例:runtime_error.rs:268。
    • 问题成立:测的不是真实数据流;它能过只是因为被调函数冗余再脱敏,等于把「实现恰好兜住了」当成「合同被验证」。
    • 修复:用例先按 8 KiB 口径 redact_agent_runtime_error 造出 safe_detail 再传入(与生产一致),metadata 仍按生产原样传未脱敏 JSON,脱敏覆盖不丢。
  • 4 身份行仍可能被整行脱敏吃掉(bug · medium)— 已按方案 A 修复(提交 74e6ac90a)

    • 现状:runtime_error.rs:181 的身份行是
      agent.runtime.error eventId=… source=… stage=… code=… retryable=… clientTurnId=… elapsedMs=… detailRef=… summary=<public_text>
      其中 source / stage / code 是调用方给的常量,clientTurnId / detailRef / eventId / elapsedMs 由程序生成,自由文本只有 summary 这一处
    • 问题成立(第 1 条修完仍有残余):sanitize_diagnostic_message(main.rs:1922-1946)的判定是「整行 lowercase 命中 authorization / bearer / api_key / apikey / api key / x-api-key / token= / token: / credential 任一 → 整行替换成 <sensitive diagnostic details redacted>(信息全丢)」。第 1 条的脱敏能消掉赋值形式与 bearer+value,但裸词消不掉:例如工具错误原文里出现 “credential rotation failed” 或 “authorization header missing”,summary 原样保留该词,身份行连 eventId / detailRef 一起消失——正是拆两行想避免的那种失败,在身份行自己身上重演。
    • 落实(方案 A):身份行只留程序生成或调用方常量字段(eventId / source / stage / code / retryable / clientTurnId / elapsedMs / detailRef),summary 移到详情行(hint / summary / detail / metadata);自由文本从此只出现在详情行,整行替换最多吃掉详情行。同步了两份技术方案、决策记录与踩坑记录的两行字段口径。
    • 用例补了裸标记词场景:summary = "credential rotation failed" 时断言身份行仍含 eventId / code,且详情行确实被替换成 <sensitive diagnostic details redacted>。验证:cargo test -- runtime_error direct_tool_bridge(41 passed)。
    • 前提:身份行现在没有外来自由文本,实际不可再被标记规则整行吃掉;以后新增调用方不要把自由文本塞进 source / stage / code / clientTurnId
- [x] 1 身份行 summary 只压行未脱敏(security · medium) - 现状(修复前):`agent_runtime_error_app_log_lines` 对 `public_text` 只做 `single_line_log_field`;身份行 `summary=` 原样进 `app_log!`。现在该行:runtime_error.rs:173。 - 问题成立:`app_log!`(main.rs:73)先 `format!` 再 `eprintln!`,stderr 这条路径**没有** `sanitize_diagnostic_message`;而 `direct_tool_bridge.rs:3348` 把工具错误原文 `result.pointer("/content/0/text")` 同时当 `public_text` 与 `detail` 传进来,内容不可控。落盘那份虽有兜底,但 stderr 会漏原文。 - 修复:新增 `AGENT_RUNTIME_ERROR_APP_LOG_SUMMARY_CHARS = 320`(与 sidecar 摘要预算同口径),summary 先 `redact_agent_runtime_error` 再压行;函数合同改为「summary / detail / metadata 三类外来文本都在这里脱敏」,不再只是压平换行。同步了两份技术文档与 decision-log 的预算口径。 - 未一并处理:调用侧(第 7 条)仍把原文当 public_text,属用户可见文本,留给你。 - [x] 2 metadata 没走压行(maintainability · low) - 现状(修复前):详情行直接 `metadata={metadata}` 插值,只靠 `Value::to_string()` 的 JSON 转义保证单行。现在该行:runtime_error.rs:190。 - 问题成立:与函数「每个字段都先压平」的约定不一致,把日志行完整性绑在序列化器的转义行为上(今天安全,但契约上说不通)。 - 修复:`metadata` 与其他字段一样过 `single_line_log_field`。 - [x] 3 用例先 join 再 sanitize,测不到逐行行为(test · low) - 现状(修复前):用例把两行 `join(" ")` 后只 sanitize 一次。现在该用例:runtime_error.rs:262。 - 问题成立:生产是 `app_log!` → `append_application_log_line` 逐行脱敏与逐行截断(`chars().take(2_048)`),拼接后测不到「只该脱敏其中一行」「身份行被截断」这两类。 - 修复:改为逐行 sanitize,再分别断言身份行(eventId / code / detailRef / summary)与详情行(detail 的路径与 URL 已脱敏、metadata 无凭据),长度上限仍逐行核。 - [x] 5 「只再截一次」的注释与实现不符(maintainability · low) - 现状(修复前):注释写「传进来的 detail 已经是脱敏文本,这里只再按预算截一次」,实现却对 `detail` 跑完整 `redact_agent_runtime_error`(含 `root.canonicalize()` 与标记保留逻辑)。现在注释:runtime_error.rs:137。 - 症状成立:合同与实现确实不一致。 - 处理:**未采纳**「去掉再脱敏、只做截断」的建议,改为把合同写实——summary / detail / metadata 都在这里过一遍脱敏,已脱敏的 detail 也照走。理由:① 截断会把 `[redacted-secret]` 这类标记切开,不能假设截断后的文本仍满足前置条件;② 这是 `pub(crate)` 边界,不应把安全性绑定在调用方自觉上;③ 代价只是一次可忽略的 canonicalize(只在终态失败时发生)。 - 若你更看重那点 I/O 与「单一职责」,可以把 detail 的分支换成纯截断,但那要先接受上面两条前提。 - [x] 6 用例传的是未脱敏原文,与前置条件不符(test · low) - 现状(修复前):用例直接传含 `token=secret` 的原文 detail,而生产 `persist_agent_runtime_error` 传的是已脱敏的 `safe_detail`。现在该用例:runtime_error.rs:268。 - 问题成立:测的不是真实数据流;它能过只是因为被调函数冗余再脱敏,等于把「实现恰好兜住了」当成「合同被验证」。 - 修复:用例先按 8 KiB 口径 `redact_agent_runtime_error` 造出 `safe_detail` 再传入(与生产一致),`metadata` 仍按生产原样传未脱敏 JSON,脱敏覆盖不丢。 - [x] 4 身份行仍可能被整行脱敏吃掉(bug · medium)— 已按方案 A 修复(提交 74e6ac90a) - 现状:runtime_error.rs:181 的身份行是 `agent.runtime.error eventId=… source=… stage=… code=… retryable=… clientTurnId=… elapsedMs=… detailRef=… summary=<public_text>`。 其中 source / stage / code 是调用方给的常量,clientTurnId / detailRef / eventId / elapsedMs 由程序生成,**自由文本只有 summary 这一处**。 - 问题成立(第 1 条修完仍有残余):`sanitize_diagnostic_message`(main.rs:1922-1946)的判定是「整行 lowercase 命中 `authorization` / `bearer ` / `api_key` / `apikey` / `api key` / `x-api-key` / `token=` / `token:` / `credential` 任一 → 整行替换成 `<sensitive diagnostic details redacted>`(信息全丢)」。第 1 条的脱敏能消掉**赋值形式**与 bearer+value,但**裸词**消不掉:例如工具错误原文里出现 “credential rotation failed” 或 “authorization header missing”,summary 原样保留该词,身份行连 eventId / detailRef 一起消失——正是拆两行想避免的那种失败,在身份行自己身上重演。 - 落实(方案 A):身份行只留程序生成或调用方常量字段(eventId / source / stage / code / retryable / clientTurnId / elapsedMs / detailRef),`summary` 移到详情行(hint / summary / detail / metadata);自由文本从此只出现在详情行,整行替换最多吃掉详情行。同步了两份技术方案、决策记录与踩坑记录的两行字段口径。 - 用例补了裸标记词场景:`summary = "credential rotation failed"` 时断言身份行仍含 `eventId` / `code`,且详情行确实被替换成 `<sensitive diagnostic details redacted>`。验证:`cargo test -- runtime_error direct_tool_bridge`(41 passed)。 - 前提:身份行现在没有外来自由文本,实际不可再被标记规则整行吃掉;以后新增调用方不要把自由文本塞进 `source` / `stage` / `code` / `clientTurnId`。
k88936 merged commit 1b951619c9 into master 2026-09-22 10:54:42 +08:00
k88936 deleted branch feat/log-the-dianosis 2026-09-22 10:54:43 +08:00
Sign in to join this conversation.