直连工具结果与计划回灌不再脱敏,只做长度收口

- `direct_validation`:浏览器/命令验证回执里的 error/output 原文返回给 Codex
- `direct_patch`:补丁工具输出原文返回
- `direct_delivery`:计划步文案、`contract.scope`、未过验收的缺口清单原文返回
- `direct_runtime`:浏览器事实证据(diagnostic / console.error / exception / fatal request)、陶泥儿整包完成告警、可返修错误的反馈提示原文返回
- `provider_request_builders`:回灌给模型的 `thinkingSummary` / `fallbackResponse` 原文返回
- 两个测试改为断言原文进入 prompt(`browser_diagnostics_go_verbatim_to_the_model_before_same_thread_repair`、`completed_regeneration_replays_the_exact_lost_response`)
- 去掉因不再脱敏而失去用途的 `root` 形参:`update_plan`、`bound_for_completed_workflow_at` / `validate_completed_workflow_result_at`
- 审计/诊断侧照旧脱敏:`record_direct_codex_failure`、`DirectTurnTerminal::failed`、`persist_agent_runtime_error`、`codex_app_server` 日志
This commit is contained in:
2026-10-01 19:21:57 +08:00
parent b0ec155401
commit 7224bd0dc0
6 changed files with 43 additions and 55 deletions
@@ -35,7 +35,6 @@ enum DirectPlanStepStatus {
}
pub(super) async fn update_plan(
root: &Path,
session: &Arc<ExecutionSession>,
input: &Value,
) -> Result<Value, String> {
@@ -51,13 +50,13 @@ pub(super) async fn update_plan(
{
return Err("direct-plan-invalid: 每步需为1到500字".into());
}
item.step = redact_agent_runtime_error(root, item.step.trim(), 500);
item.step = truncate_agent_runtime_text(item.step.trim(), 500);
}
if let Some(explanation) = &mut plan.explanation {
if explanation.chars().count() > 1200 || explanation.contains('\0') {
return Err("direct-plan-invalid: 说明超过上限".into());
}
*explanation = redact_agent_runtime_error(root, explanation, 1200);
*explanation = truncate_agent_runtime_text(explanation, 1200);
}
let value = serde_json::to_value(plan).map_err(|_| "direct-plan-invalid")?;
let session = Arc::clone(session);
@@ -689,7 +688,7 @@ pub(super) async fn finish_sealing(
session.reopen_for_repair()?; return Ok(None);
}
let contract: FrozenContract = serde_json::from_value(ledger.contract.clone().ok_or("delivery-contract-missing")?).map_err(|_| "delivery-contract-invalid")?;
let report = format!("本轮已完成宿主验收。\n\n范围:{}\n\n{}\n\n执行/返修批次:{}/{}。仅证明以上登记范围;固定场景不代表完整长关卡已通关。执行预算按宿主观察的占用计入;第三方并发调用可能按组占用上界计入。", redact_agent_runtime_error(&root,&contract.scope,1200),
let report = format!("本轮已完成宿主验收。\n\n范围:{}\n\n{}\n\n执行/返修批次:{}/{}。仅证明以上登记范围;固定场景不代表完整长关卡已通关。执行预算按宿主观察的占用计入;第三方并发调用可能按组占用上界计入。", truncate_agent_runtime_text(&contract.scope,1200),
final_check.checks.iter().map(|check|format!("- 已通过:{}",check["label"].as_str().unwrap_or("验收项"))).collect::<Vec<_>>().join("\n"), ledger.used_passes,ledger.max_runs);
session.complete(report.clone())?;
if mark_initial_delivered(&root,&ledger).is_err() { app_log!("首次交付标记未写入,后续保持更严格的新项目验收要求"); }
@@ -746,7 +745,7 @@ pub(super) async fn review_reply(
.join("\n");
let report = format!(
"本轮未完成宿主验收,已到有界复核上限。保留已有修改与证据;仍未通过:\n{}",
redact_agent_runtime_error(root, &missing, 6000)
truncate_agent_runtime_text(&missing, 6000)
);
let session = Arc::clone(session);
let saved = report.clone();
@@ -830,23 +829,21 @@ mod tests {
let plan =
json!({"explanation":"任务进度", "plan":[{"step":"已完成", "status":"completed"}]});
assert_eq!(
update_plan(root.path(), &session, &plan).await.unwrap()["acceptancePassed"],
update_plan(&session, &plan).await.unwrap()["acceptancePassed"],
false
);
assert!(!try_seal(root.path(), &session).await.unwrap());
assert_eq!(session.snapshot().unwrap().contract, contract);
let mut forged = plan.clone();
forged["passed"] = json!(true);
assert!(update_plan(root.path(), &session, &forged).await.is_err());
assert!(update_plan(&session, &forged).await.is_err());
assert!(update_plan(
root.path(),
&session,
&json!({"plan":[{"step":"task", "status":"passed"}]})
)
.await
.is_err());
assert!(update_plan(
root.path(),
&session,
&json!({"plan":[{"step":"a".repeat(501), "status":"pending"}]})
)
@@ -332,7 +332,7 @@ fn run_transaction(
.collect::<BTreeSet<_>>();
let changed = started && !changed_paths.is_empty();
let mut uncertain = needs_reconciliation || timed_out;
let mut output = redact_agent_runtime_error(root, &output, 6000);
let mut output = truncate_agent_runtime_text(&output, 6000);
let passed = exit_code == Some(0) && !uncertain;
if uncertain {
session.interrupt(
@@ -361,23 +361,21 @@ pub(crate) struct DirectTaonierArtPackage {
}
impl DirectTaonierArtPackage {
fn redact_for_completed_workflow_at(mut self, root: &Path) -> Result<Self, String> {
/// 告警原文给模型,只做长度收口;脱敏只发生在写审计/日志的那一份。
fn bound_for_completed_workflow_at(mut self) -> Result<Self, String> {
for warning in self
.warnings
.iter_mut()
.chain(self.slice_warnings.iter_mut())
{
*warning = redact_agent_runtime_error(
root,
warning,
DIRECT_TAONIER_COMPLETED_WARNING_MAX_CHARS,
);
*warning =
truncate_agent_runtime_text(warning, DIRECT_TAONIER_COMPLETED_WARNING_MAX_CHARS);
}
self.validate_completed_workflow_result_at(root)?;
self.validate_completed_workflow_result_at()?;
Ok(self)
}
fn validate_completed_workflow_result_at(&self, root: &Path) -> Result<(), String> {
fn validate_completed_workflow_result_at(&self) -> Result<(), String> {
let expected_asset_paths = DIRECT_CODEX_ART_ASSET_PATHS
.iter()
.map(|path| (*path).to_string())
@@ -398,14 +396,8 @@ impl DirectTaonierArtPackage {
return Err("陶泥儿整包重生成完成结果的告警数量超过上限".to_string());
}
for warning in self.warnings.iter().chain(self.slice_warnings.iter()) {
if warning.chars().count() > DIRECT_TAONIER_COMPLETED_WARNING_MAX_CHARS
|| redact_agent_runtime_error(
root,
warning,
DIRECT_TAONIER_COMPLETED_WARNING_MAX_CHARS,
) != *warning
{
return Err("陶泥儿整包重生成完成结果包含未脱敏或超限告警".to_string());
if warning.chars().count() > DIRECT_TAONIER_COMPLETED_WARNING_MAX_CHARS {
return Err("陶泥儿整包重生成完成结果的告警超过长度上限".to_string());
}
}
Ok(())
@@ -574,7 +566,7 @@ fn read_direct_taonier_regeneration_workflow_at(
rollback.validate()?;
}
if let Some(result) = workflow.completed_result.as_ref() {
result.validate_completed_workflow_result_at(root)?;
result.validate_completed_workflow_result_at()?;
}
match workflow.state {
DirectTaonierRegenerationWorkflowState::Completed
@@ -1266,7 +1258,7 @@ fn complete_direct_taonier_regeneration_workflow_locked_at(
expected: &DirectTaonierRegenerationWorkflow,
result: DirectTaonierArtPackage,
) -> Result<DirectTaonierArtPackage, String> {
let result = result.redact_for_completed_workflow_at(root)?;
let result = result.bound_for_completed_workflow_at()?;
if !direct_taonier_strict_art_package_is_valid(root)
|| direct_registered_taonier_slice_paths(root) != result.slice_paths
{
@@ -3614,7 +3606,7 @@ fn direct_browser_evidence_prompt(
completion_error: Option<&str>,
) -> String {
let completion_status = completion_error
.map(|error| redact_agent_runtime_error(root, error, 320))
.map(|error| truncate_agent_runtime_text(error, 320))
.unwrap_or_else(|| prompt_text!("direct.browser.noCompletionError").to_string());
let (browser_status, viewport_lines, details, advisory) = match result {
Some(result) => {
@@ -3651,7 +3643,7 @@ fn direct_browser_evidence_prompt(
.iter()
.flat_map(|viewport| viewport.diagnostics.iter())
.filter(|diagnostic| diagnostic.starts_with("[advisory]"))
.map(|diagnostic| redact_agent_runtime_project_paths(root, diagnostic, 240))
.map(|diagnostic| truncate_agent_runtime_text(diagnostic, 240))
.collect::<Vec<_>>();
let details = result
.viewport_results
@@ -3670,20 +3662,20 @@ fn direct_browser_evidence_prompt(
.map(|diagnostic| {
format!(
"{viewport_name} diagnostic: {}",
redact_agent_runtime_error(root, diagnostic, 320)
truncate_agent_runtime_text(diagnostic, 320)
)
}),
);
details.extend(viewport.console_errors.iter().take(4).map(|message| {
format!(
"{viewport_name} console.error: {}",
redact_agent_runtime_error(root, &message.text, 320)
truncate_agent_runtime_text(&message.text, 320)
)
}));
details.extend(viewport.exceptions.iter().take(4).map(|exception| {
format!(
"{viewport_name} exception: {}",
redact_agent_runtime_error(root, &exception.text, 320)
truncate_agent_runtime_text(&exception.text, 320)
)
}));
details.extend(
@@ -3696,8 +3688,8 @@ fn direct_browser_evidence_prompt(
format!(
"{viewport_name} fatal request: {} {} ({})",
request.method,
redact_agent_runtime_error(root, &request.url, 320),
redact_agent_runtime_error(root, &request.error_text, 220),
truncate_agent_runtime_text(&request.url, 320),
truncate_agent_runtime_text(&request.error_text, 220),
)
}),
);
@@ -3904,7 +3896,7 @@ fn write_direct_browser_acceptance_summary(
.diagnostics
.iter()
.take(12)
.map(|detail| redact_agent_runtime_project_paths(root, detail, 320))
.map(|detail| truncate_agent_runtime_text(detail, 320))
.collect::<Vec<_>>();
let rendered_assets_by_viewport = evidence
.viewport_results
@@ -3982,7 +3974,7 @@ fn render_direct_browser_acceptance_report(
.iter()
.flat_map(|viewport| viewport.diagnostics.iter())
.filter(|diagnostic| diagnostic.starts_with("[advisory]"))
.map(|diagnostic| redact_agent_runtime_project_paths(root, diagnostic, 180))
.map(|diagnostic| truncate_agent_runtime_text(diagnostic, 180))
.collect::<Vec<_>>();
let rendered_assets = evidence
.viewport_results
@@ -4799,7 +4791,7 @@ async fn run_direct_game_creator_turn_inner(
if attempt < DIRECT_CODEX_ERROR_FEEDBACK_MAX_ATTEMPTS
&& error.is_model_repairable() =>
{
let detail = redact_agent_runtime_error(root, &error.to_string(), 1800);
let detail = truncate_agent_runtime_text(&error.to_string(), 1800);
emitter.emit(
"running",
Some("error-feedback"),
@@ -4868,7 +4860,7 @@ async fn run_direct_game_creator_turn_inner(
if attempt < DIRECT_CODEX_ERROR_FEEDBACK_MAX_ATTEMPTS
&& error.is_model_repairable() =>
{
let detail = redact_agent_runtime_error(root, &error.to_string(), 1800);
let detail = truncate_agent_runtime_text(&error.to_string(), 1800);
attempt += 1;
feedback_prompt = direct_codex_error_feedback_prompt(&detail);
}
@@ -6610,7 +6602,7 @@ mod tests {
}
#[tokio::test]
async fn completed_regeneration_replays_the_exact_redacted_lost_response() {
async fn completed_regeneration_replays_the_exact_lost_response() {
let root = tempfile::tempdir().expect("temp dir");
init_local_game_project_at(root.path(), "direct-workflow-cleanup", "整包账本清理")
.expect("init project");
@@ -6669,8 +6661,9 @@ mod tests {
)
.expect("complete workflow");
assert_eq!(completed_result.slice_warnings, vec!["切片告警保持原样"]);
assert!(!completed_result.warnings[0].contains("secret.example"));
assert!(!completed_result.warnings[0].contains(root.path().to_string_lossy().as_ref()));
// 完成结果原文交回模型:URL 与项目绝对路径不再被抹掉,只做长度收口。
assert!(completed_result.warnings[0].contains("secret.example"));
assert!(completed_result.warnings[0].contains(root.path().to_string_lossy().as_ref()));
assert!(stage_paths.iter().all(|path| path.is_file()));
let persisted = read_direct_taonier_regeneration_workflow_at(root.path())
.expect("read completed workflow")
@@ -7528,7 +7521,7 @@ mod tests {
}
#[test]
fn browser_diagnostics_are_redacted_before_same_thread_repair() {
fn browser_diagnostics_go_verbatim_to_the_model_before_same_thread_repair() {
let root = tempfile::tempdir().expect("temp dir");
let browser_error = format!(
"Uncaught ReferenceError at {}?token=secret",
@@ -7586,9 +7579,10 @@ mod tests {
assert!(prompt.contains("desktop exception:"));
assert!(prompt.contains("AGC 浏览器事实证据"));
assert!(!prompt.contains("token=secret"), "{prompt}");
// 事实证据原文进 prompt:URL 里的 token 与项目绝对路径不再被抹掉,只做长度收口。
assert!(prompt.contains("token=secret"), "{prompt}");
assert!(
!prompt.contains(&root.path().display().to_string()),
prompt.contains(&root.path().display().to_string()),
"{prompt}"
);
}
@@ -3406,7 +3406,7 @@ async fn bridge_update_plan(
))
}
};
let value = super::direct_delivery::update_plan(&state.root, &session, arguments)
let value = super::direct_delivery::update_plan(&session, arguments)
.await
.map_err(|cause| UpdatePlanError::PlanRejected { cause })?;
Ok(bridge_tool_result(value.to_string(), Vec::new()))
@@ -495,7 +495,7 @@ async fn run_browser_with_budget(
)
}
Err(error) => (
json!({"mode":input.mode,"needsReconciliation":error.starts_with("browser-cleanup-unconfirmed:"),"error":redact_agent_runtime_error(root, &error, 1800)}),
json!({"mode":input.mode,"needsReconciliation":error.starts_with("browser-cleanup-unconfirmed:"),"error":truncate_agent_runtime_text(&error, 1800)}),
false,
),
};
@@ -585,13 +585,13 @@ pub(super) async fn run_command(root: &Path, arguments: &Value) -> Result<Value,
(
json!({"kind":"command", "mode":"command", "program":input.program,"args":input.arguments,"cwd":input.cwd,"purpose":input.purpose,
"exitCode":result.exit_code,"timedOut":result.timed_out,
"durationMs":result.duration_ms,"output":redact_agent_runtime_error(root,&result.output,12_000),
"durationMs":result.duration_ms,"output":truncate_agent_runtime_text(&result.output,12_000),
"outputTruncated":result.capture_truncated,"coverage":"command-exit-status"}),
passed,
)
}
Err(error) => (
json!({"mode":"command","needsReconciliation":error.needs_reconciliation(),"error":redact_agent_runtime_error(root,&error.to_string(),1800)}),
json!({"mode":"command","needsReconciliation":error.needs_reconciliation(),"error":truncate_agent_runtime_text(&error.to_string(),1800)}),
false,
),
};
@@ -632,13 +632,10 @@ pub(in crate::agent) fn build_game_creator_agent_background_final_reply_request(
)?;
let observations_json = serde_json::to_string_pretty(&prompt_observations)
.map_err(|error| format!("序列化 Agent 工具观察失败:{error}"))?;
// 回灌给模型的是它自己上一轮的计划原文,只做长度收口;脱敏只发生在写审计/日志时。
let plan_json = serde_json::to_string_pretty(&serde_json::json!({
"thinkingSummary": redact_agent_runtime_project_paths(
root,
&plan.thinking_summary,
240,
),
"fallbackResponse": redact_agent_runtime_project_paths(root, &plan.response, 1_200),
"thinkingSummary": truncate_agent_runtime_text(&plan.thinking_summary, 240),
"fallbackResponse": truncate_agent_runtime_text(&plan.response, 1_200),
}))
.map_err(|error| format!("序列化 Agent 收束摘要失败:{error}"))?;
let steers_json =