Treat user input answers as Guardian authorization changes (#40623)

## Why

Answers collected by `request_user_input` can change what the user authorizes, but post-tool hooks may replace or reject the tool output that Guardian normally sees.

## What changed

- Record bounded, host-observed answers before post-tool hooks run and provide them as trusted evidence to synchronous and asynchronous Guardian reviews.
- Include root-thread answers in worker review context.
- Count successful answers as authorization changes so earlier review evidence becomes stale, while ignoring empty or unrelated answers.

## Testing

Add coverage for allowed and denied stale reviews, empty and oversized answers, hook-modified output, and root-to-worker propagation.

GitOrigin-RevId: 970c52e40338ecd057c7cd2a69d04b06e97cd068
This commit is contained in:
jif authored and copyberry committed 2026-08-25 15:03:46 +00:00
1 parent c395364915
commit 4b81410a80
9 files changed
+563 -72

No files matched your search

@@ -7,7 +7,6 @@ use std::time::Duration;
use std::time::Instant;
use std::time::SystemTime;
use codex_core::GuardianAuthorizationVersion;
use codex_core::GuardianRootMessage;
use codex_core::ThreadManager;
use codex_core::config::Config;
@@ -615,10 +614,10 @@ impl GuardianV2Extension {
.and_then(|model| model.auto_review_model_override.clone());
let conversation_history = Arc::clone(&input.conversation_history);
// Snapshot before spawning so a delayed sample cannot see later reviews.
let sync_reviews = input
let guardian_evidence = input
.thread_store
.get_or_init(GuardianReviewEvidence::default)
.snapshot();
.get_or_init(GuardianReviewEvidence::default);
let sync_reviews = guardian_evidence.snapshot();
let node_repl_images = if guardian_config.transcript.include_images {
input
.thread_store
@@ -637,7 +636,9 @@ impl GuardianV2Extension {
.map(|snapshot| snapshot.authorization_version);
let root_conversation = root_snapshot.map(|snapshot| snapshot.messages);
let authorization_version =
GuardianAuthorizationVersion::from_history(conversation_history.as_ref());
guardian_evidence.authorization_version(conversation_history.as_ref());
let trusted_user_inputs =
guardian_evidence.user_input_fragments(conversation_history.as_ref());
let transcript = guardian_config
.transcript
.build(conversation_history.items());
@@ -691,6 +692,11 @@ impl GuardianV2Extension {
);
classification_input.push(">>> ROOT CONVERSATION END\n".to_owned());
}
if !trusted_user_inputs.is_empty() {
classification_input.push(">>> TRUSTED USER ANSWERS START\n".to_owned());
classification_input.extend(trusted_user_inputs);
classification_input.push(">>> TRUSTED USER ANSWERS END\n".to_owned());
}
classification_input.push(">>> TRANSCRIPT START\n".to_owned());
classification_input.extend(transcript.entries);
classification_input.push(">>> TRANSCRIPT END\n\n".to_owned());