Merge branch 'master' into feat/agc_track
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m55s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m4s
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m55s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m4s
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
//!
|
||||
//! Every caller supplies a safe public summary and a private detail. This
|
||||
//! module is the only persistence boundary for the latter: it redacts project
|
||||
//! paths and credentials before writing a bounded diagnostic sidecar.
|
||||
//! paths and credentials before writing a bounded diagnostic sidecar, and
|
||||
//! projects the same bounded diagnosis into the AppData application log.
|
||||
|
||||
use super::{redact_agent_runtime_error, write_agent_runtime_json_sidecar_with_max_bytes};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -14,6 +15,20 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
||||
pub(crate) const AGENT_RUNTIME_ERROR_SCHEMA_VERSION: &str = "agent-runtime-error.v1";
|
||||
pub(crate) const AGENT_RUNTIME_ERROR_MAX_DETAIL_CHARS: usize = 8 * 1024;
|
||||
|
||||
/// 应用日志里 detail / metadata 的字符预算。
|
||||
///
|
||||
/// `application.log` 的每一行在落盘前还会被 `sanitize_diagnostic_message` 截到 2 KiB,
|
||||
/// 这里的预算留出身份字段与中文摘要的位置,保证被截掉的是诊断正文的尾部,而不是
|
||||
/// `eventId`、`code` 或 `detailRef`。
|
||||
pub(crate) const AGENT_RUNTIME_ERROR_APP_LOG_DETAIL_CHARS: usize = 1_200;
|
||||
pub(crate) const AGENT_RUNTIME_ERROR_APP_LOG_METADATA_CHARS: usize = 200;
|
||||
|
||||
/// 详情行里 public summary 的字符预算。
|
||||
///
|
||||
/// `summary` 由调用方给,`direct_tool_bridge` 传的是工具错误原文;这里与 sidecar 侧的摘要
|
||||
/// 预算同口径(320 字符)截断,并再脱敏一次,避免摘要把整条详情行占满。
|
||||
pub(crate) const AGENT_RUNTIME_ERROR_APP_LOG_SUMMARY_CHARS: usize = 320;
|
||||
|
||||
static ERROR_EVENT_SEQUENCE: AtomicU64 = AtomicU64::new(1);
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||
@@ -71,6 +86,27 @@ pub(crate) fn persist_agent_runtime_error(
|
||||
"detail": safe_detail,
|
||||
"metadata": metadata,
|
||||
});
|
||||
// 统一错误事件的项目内 sidecar 只在项目目录可见:用户提交错误报告时上传的是 AppData
|
||||
// 应用日志,诊断包拿不到 detail。这里先把同一份已脱敏诊断留进应用日志,再落项目文件,
|
||||
// 于是 sidecar 写失败也仍然留下可提交的诊断。
|
||||
let app_log_lines = agent_runtime_error_app_log_lines(
|
||||
root,
|
||||
&event_id,
|
||||
client_turn_id,
|
||||
source,
|
||||
stage,
|
||||
code,
|
||||
retryable,
|
||||
public_text,
|
||||
recovery_hint,
|
||||
&detail_ref,
|
||||
&safe_detail,
|
||||
elapsed_ms,
|
||||
&metadata,
|
||||
);
|
||||
for line in app_log_lines {
|
||||
app_log!("{line}");
|
||||
}
|
||||
write_agent_runtime_json_sidecar_with_max_bytes(
|
||||
root,
|
||||
&detail_ref,
|
||||
@@ -96,6 +132,78 @@ pub(crate) fn persist_agent_runtime_error(
|
||||
})
|
||||
}
|
||||
|
||||
/// 把统一错误事件投影成 AppData `diagnostics/application.log` 里的两行。
|
||||
///
|
||||
/// 传进来的 `detail` 是 sidecar 用的脱敏文本,`metadata` 则从未脱敏过。两行落到 `app_log!`
|
||||
/// 时都会先按应用日志预算(1200 / 200 字符)再脱敏、再截断:`app_log!` 还会把同一行写到
|
||||
/// stderr,那里没有 `sanitize_diagnostic_message` 兜底,所以每个调用方给的外来文本
|
||||
/// (`summary` 按 320 字符预算)都在这里过一遍脱敏。
|
||||
///
|
||||
/// 已经脱敏过的 `detail` 也照走同一遍流水线,不按「调用方已脱敏」走短路:截断会把
|
||||
/// `[redacted-secret]` 这类标记切开,而且这里是 `pub(crate)` 边界,不假设未来调用方一定先脱敏。
|
||||
///
|
||||
/// 拆成「身份行 + 详情行」是因为整行只要出现凭据标记就会被
|
||||
/// [`crate::sanitize_diagnostic_message`] 整体替换成脱敏占位。因此身份行**只放程序生成或
|
||||
/// 调用方常量字段**(eventId / source / stage / code / retryable / clientTurnId / elapsedMs /
|
||||
/// detailRef),`summary`、`hint` 这些自由文本全部放详情行:自由文本里一个裸词
|
||||
/// (例如 `credential`)就能让整行被替换,放错了就会把事件定位信息一起吃掉。
|
||||
///
|
||||
/// 单行口径在这里落地:`app_log!` 同时把这行写到 stderr,那里没有人替我们压行,
|
||||
/// 所以每个字段(含 `source` / `stage` / `code` / `detailRef` 这些调用方给的标识)都先
|
||||
/// 过一遍 [`single_line_log_field`],不假设调用方一定给单行文本。
|
||||
pub(crate) fn agent_runtime_error_app_log_lines(
|
||||
root: &Path,
|
||||
event_id: &str,
|
||||
client_turn_id: Option<&str>,
|
||||
source: &str,
|
||||
stage: &str,
|
||||
code: &str,
|
||||
retryable: bool,
|
||||
public_text: &str,
|
||||
recovery_hint: &str,
|
||||
detail_ref: &str,
|
||||
detail: &str,
|
||||
elapsed_ms: Option<u64>,
|
||||
metadata: &Value,
|
||||
) -> [String; 2] {
|
||||
let event_id = single_line_log_field(event_id);
|
||||
let client_turn_id = single_line_log_field(client_turn_id.unwrap_or("none"));
|
||||
let source = single_line_log_field(source);
|
||||
let stage = single_line_log_field(stage);
|
||||
let code = single_line_log_field(code);
|
||||
let detail_ref = single_line_log_field(detail_ref);
|
||||
let public_text = single_line_log_field(&redact_agent_runtime_error(
|
||||
root,
|
||||
public_text,
|
||||
AGENT_RUNTIME_ERROR_APP_LOG_SUMMARY_CHARS,
|
||||
));
|
||||
let elapsed_ms = elapsed_ms
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_else(|| "none".to_string());
|
||||
let identity = format!(
|
||||
"agent.runtime.error eventId={event_id} source={source} stage={stage} code={code} retryable={retryable} clientTurnId={client_turn_id} elapsedMs={elapsed_ms} detailRef={detail_ref}"
|
||||
);
|
||||
let detail = redact_agent_runtime_error(root, detail, AGENT_RUNTIME_ERROR_APP_LOG_DETAIL_CHARS);
|
||||
let metadata = redact_agent_runtime_error(
|
||||
root,
|
||||
&metadata.to_string(),
|
||||
AGENT_RUNTIME_ERROR_APP_LOG_METADATA_CHARS,
|
||||
);
|
||||
let detail_line = format!(
|
||||
"agent.runtime.error.detail eventId={event_id} hint={} summary={} detail={} metadata={}",
|
||||
single_line_log_field(recovery_hint),
|
||||
single_line_log_field(&public_text),
|
||||
single_line_log_field(&detail),
|
||||
single_line_log_field(&metadata),
|
||||
);
|
||||
[identity, detail_line]
|
||||
}
|
||||
|
||||
/// 应用日志是逐行读取的:落到日志里的自由文本必须先压平换行。
|
||||
fn single_line_log_field(value: &str) -> String {
|
||||
value.replace(['\r', '\n'], " ")
|
||||
}
|
||||
|
||||
pub(crate) fn classify_direct_codex_error(error: &str) -> &'static str {
|
||||
let normalized = error.to_ascii_lowercase();
|
||||
if normalized.contains("等待 turn/completed 超时") {
|
||||
@@ -153,6 +261,91 @@ mod tests {
|
||||
assert!(!text.contains("token=secret"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_event_app_log_lines_keep_identity_and_redact_detail() {
|
||||
let parent = tempfile::tempdir().expect("temp root");
|
||||
let root = parent.path().join("project");
|
||||
crate::project::init_local_game_project_at(&root, "runtime-error", "错误事件")
|
||||
.expect("init project");
|
||||
// 生产路径传进来的是已经脱敏的 safe_detail(8 KiB 口径),这里按同一口径造输入;
|
||||
// metadata 在生产里从未脱敏,仍按原文传。
|
||||
let safe_detail = redact_agent_runtime_error(
|
||||
&root,
|
||||
"C:\\Users\\private\\project https://provider.example/a?token=secret\n第二行诊断",
|
||||
AGENT_RUNTIME_ERROR_MAX_DETAIL_CHARS,
|
||||
);
|
||||
let lines = agent_runtime_error_app_log_lines(
|
||||
&root,
|
||||
"error-1-1",
|
||||
Some("turn-123"),
|
||||
"direct-codex",
|
||||
"code-generation",
|
||||
"turn-idle-timeout",
|
||||
true,
|
||||
"本轮没有收到完成事件\n附带换行",
|
||||
"查看诊断后重试",
|
||||
".agent/runtime/errors/error-1-1.json",
|
||||
&safe_detail,
|
||||
Some(1200),
|
||||
&serde_json::json!({"authorization": "Bearer secret"}),
|
||||
);
|
||||
for line in &lines {
|
||||
assert!(!line.contains('\n'), "{line}");
|
||||
}
|
||||
// 落盘边界按真实口径核验:应用日志逐行脱敏、逐行截断,两行不会先拼在一起。
|
||||
let persisted = lines
|
||||
.iter()
|
||||
.map(|line| crate::sanitize_diagnostic_message(line, None))
|
||||
.collect::<Vec<_>>();
|
||||
for line in &persisted {
|
||||
assert!(!line.contains("token=secret"), "{line}");
|
||||
assert!(line.chars().count() <= 2_048, "{line}");
|
||||
}
|
||||
let identity = &persisted[0];
|
||||
assert!(identity.contains("eventId=error-1-1"), "{identity}");
|
||||
assert!(identity.contains("code=turn-idle-timeout"), "{identity}");
|
||||
assert!(
|
||||
identity.contains("detailRef=.agent/runtime/errors/error-1-1.json"),
|
||||
"{identity}"
|
||||
);
|
||||
// 身份行只放程序生成或调用方常量字段:自由文本放错行会让「整行命中标记」把它吃掉。
|
||||
assert!(!identity.contains("summary="), "{identity}");
|
||||
let detail = &persisted[1];
|
||||
assert!(
|
||||
detail.contains("summary=本轮没有收到完成事件 附带换行"),
|
||||
"{detail}"
|
||||
);
|
||||
assert!(
|
||||
detail.contains("detail=<absolute-path> <redacted-url> 第二行诊断"),
|
||||
"{detail}"
|
||||
);
|
||||
assert!(!detail.contains("Bearer secret"), "{detail}");
|
||||
|
||||
// 自由文本里出现裸标记词时,被整行替换的只能是详情行:身份行仍可定位事件。
|
||||
let marked = agent_runtime_error_app_log_lines(
|
||||
&root,
|
||||
"error-3-1",
|
||||
None,
|
||||
"agc-tools",
|
||||
"tool-execution",
|
||||
"tool-error",
|
||||
true,
|
||||
"credential rotation failed",
|
||||
"查看项目错误诊断后处理",
|
||||
".agent/runtime/errors/error-3-1.json",
|
||||
&safe_detail,
|
||||
None,
|
||||
&serde_json::json!({"tool": "agc_tools"}),
|
||||
);
|
||||
let identity = crate::sanitize_diagnostic_message(&marked[0], None);
|
||||
assert!(identity.contains("eventId=error-3-1"), "{identity}");
|
||||
assert!(identity.contains("code=tool-error"), "{identity}");
|
||||
assert_eq!(
|
||||
crate::sanitize_diagnostic_message(&marked[1], None),
|
||||
"<sensitive diagnostic details redacted>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timeout_and_tool_errors_have_distinct_codes() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user