Skip to main content

car_server_core/coder/
no_change.rs

1//! The gate behind `CoderState::Reported` — when a session may conclude that
2//! **no code should change**, and when it may not.
3//!
4//! ## Nomination is not adjudication
5//!
6//! The model's `report_no_change` action records a *candidate* finding. It never
7//! transitions the session. Everything that decides the outcome is checked here,
8//! outside inference, against facts the runtime collected itself.
9//!
10//! That split is the whole defence. A verdict the model can reach by giving up
11//! is an escape hatch from every hard contract, and that failure mode is worse
12//! than the gap #1070 describes. So the interesting code in this module is all
13//! refusal.
14//!
15//! ## Mutation history is monotonic
16//!
17//! [`MutationLedger`] latches on the first successful mutating tool call and
18//! never clears. Editing and later reverting does **not** restore eligibility.
19//!
20//! This is deliberately unforgiving, because the alternative is a laundry: try
21//! to fix it, fail, revert, declare the premise wrong, exit 0. The price is real
22//! and worth paying — a session that explored by editing has to keep coding or
23//! start over. A clean worktree is necessary but not sufficient; the ledger is
24//! what makes it not sufficient.
25//!
26//! ## What the runtime can actually verify
27//!
28//! Only one of the three no-change shapes, and only narrowly:
29//!
30//! - [`NoChangeKind::PremiseWrong`] — verifiable *in the operational sense that
31//!   a trusted contract already passed against the untouched baseline*. The
32//!   verdict this licenses reads "the contract was already satisfied before any
33//!   work", and NOT "the model's explanation is correct".
34//! - [`NoChangeKind::DeliberateBehavior`] — not verifiable. Passing tests
35//!   establish what the code does now, never that maintainers intended it or
36//!   still want it.
37//! - [`NoChangeKind::NonCodeDecision`] — not verifiable. Depends on policy,
38//!   ownership, rollout and priorities that live outside the worktree.
39//!
40//! A red baseline proves none of them. Source inspection and model reasoning are
41//! evidence for a human reviewer, never an independent gate.
42//!
43//! ## What the observation still cannot see, and why that is survivable
44//!
45//! [`worktree_fingerprint`] samples at shell-call *boundaries*, so a mutation
46//! made and undone inside a single invocation — `sed -i …; cargo test;
47//! git checkout -- .` — leaves `before == after` and records nothing. Writes to
48//! `.gitignore`d paths are invisible too, deliberately: `--ignored` would make
49//! every `cargo build` register as a mutation and put the terminal permanently
50//! out of reach.
51//!
52//! Both falsify the weaker claim ("this session edited nothing"). Neither
53//! reaches the terminal that matters, and the reason is worth being precise
54//! about, because it is what the design actually rests on:
55//!
56//! **the autonomous path is gated on the baseline, and the baseline is computed
57//! before the loop and held in memory.** No amount of editing, reverting,
58//! committing or hiding during the session can retro-change it. A genuinely
59//! hard task has a red baseline, so `baseline_gates_nothing` is false and the
60//! nomination routes to a human at bucket 3 — a park, never an exit-0 success.
61//! The give-up scenario this module exists to prevent therefore cannot be
62//! reached by defeating the fingerprint; it can only be reached by defeating the
63//! baseline, which the model never gets to touch.
64//!
65//! What the fingerprint and the ledger buy on top is the integrity of the
66//! *report*: a session that says "I changed nothing" should not have changed
67//! something. That is worth having, and it is a weaker claim than the terminal.
68
69use super::contract::{baseline_gates_nothing, CheckResult};
70use super::session::{ContractProvenance, NoChangeKind};
71
72/// Upper bound on the nominated text. Generous for a real finding, small enough
73/// that a transcript dump cannot be laundered into an issue body.
74pub const MAX_FINDING_TEXT: usize = 4096;
75
76/// Latches once a mutating tool has succeeded, and never clears.
77///
78/// See the module docs on why this does not reset when the worktree is restored.
79#[derive(Debug, Default)]
80pub struct MutationLedger {
81    mutated: std::sync::atomic::AtomicBool,
82}
83
84impl MutationLedger {
85    pub fn new() -> Self {
86        Self::default()
87    }
88
89    /// Record that a mutating tool call succeeded. Idempotent, and one-way.
90    pub fn record_mutation(&self) {
91        self.mutated
92            .store(true, std::sync::atomic::Ordering::SeqCst);
93    }
94
95    pub fn has_mutated(&self) -> bool {
96        self.mutated.load(std::sync::atomic::Ordering::SeqCst)
97    }
98}
99
100/// A cheap content-sensitive summary of the worktree's state.
101///
102/// Two git reads: `git status --porcelain -uall` (which paths are modified,
103/// staged, or untracked) and `git diff HEAD` (the tracked content itself). Their
104/// hash changes whenever a tool edits, creates or deletes anything git can see.
105///
106/// **Known gap**: editing the *contents* of a file that is already untracked
107/// does not move the hash, because `status` lists the path and `diff HEAD` does
108/// not cover it. That is a narrow hole and it is not the one that matters —
109/// a nomination also requires a clean worktree, and an untracked file's mere
110/// existence already shows up in `status`, so a session cannot arrive at a
111/// "clean" fingerprint while carrying an edit git would report.
112///
113/// Returns `None` when git cannot answer, which callers must treat as
114/// "cannot establish cleanliness" rather than as clean.
115pub fn worktree_fingerprint(dir: &std::path::Path) -> Option<String> {
116    use sha2::{Digest, Sha256};
117    let mut hasher = Sha256::new();
118    for part in observation(dir)? {
119        hasher.update(&part);
120        hasher.update([0u8]);
121    }
122    Some(format!("{:x}", hasher.finalize()))
123}
124
125/// The four git reads that together define "this session changed nothing".
126///
127/// Each one closes a hole the others do not, and all four were found by
128/// reproducing the bypass against a real repository rather than by reasoning:
129///
130/// - `rev-parse HEAD` — **commit identity**. Without it,
131///   `sed -i … && git add -A && git commit` leaves `status` and `diff HEAD`
132///   both empty, so a real, persistent edit is invisible. The only thing
133///   standing against that was a line in the system prompt asking the model not
134///   to commit, and a prompt is not a control: under this feature's own threat
135///   model the reader is a model looking for a non-loss exit.
136/// - `status --porcelain -uall` — modified, staged and untracked paths.
137/// - `diff HEAD` — tracked content, so an edit staged *and* unstaged still shows.
138/// - `ls-files -v` — **index flags**. `git update-index --assume-unchanged f`
139///   changes neither status nor diff, and every later edit to that path is then
140///   invisible to both, permanently. The flag lives in `.git/`, outside
141///   everything else here. Lowercase tags and `S` mark it.
142fn observation(dir: &std::path::Path) -> Option<Vec<Vec<u8>>> {
143    let read = |args: &[&str]| -> Option<Vec<u8>> {
144        let out = std::process::Command::new("git")
145            .current_dir(dir)
146            .args(args)
147            .output()
148            .ok()?;
149        out.status.success().then_some(out.stdout)
150    };
151    Some(vec![
152        read(&["rev-parse", "HEAD"])?,
153        read(&["status", "--porcelain", "-uall"])?,
154        read(&["diff", "HEAD"])?,
155        read(&["ls-files", "-v"])?,
156    ])
157}
158
159/// Whether any path carries an index flag that hides later edits from
160/// `status`/`diff` — `assume-unchanged` (lowercase tag) or `skip-worktree`
161/// (`S`). `git ls-files -v` prefixes each path with its tag.
162fn has_hiding_index_flags(ls_files_v: &[u8]) -> bool {
163    String::from_utf8_lossy(ls_files_v).lines().any(|line| {
164        line.chars()
165            .next()
166            .is_some_and(|c| c == 'S' || c.is_ascii_lowercase())
167    })
168}
169
170/// Whether the worktree currently carries no change git can see.
171///
172/// `None` when git cannot answer. A caller must treat that as "cannot establish
173/// cleanliness", never as clean — the whole point of the check is that the
174/// burden of proof sits on the session claiming it changed nothing.
175pub fn worktree_is_pristine(dir: &std::path::Path, expected_head: &str) -> Option<bool> {
176    let parts = observation(dir)?;
177    let head = String::from_utf8_lossy(&parts[0]).trim().to_string();
178    Some(
179        head == expected_head.trim()
180            && parts[1].is_empty()
181            && parts[2].is_empty()
182            && !has_hiding_index_flags(&parts[3]),
183    )
184}
185
186/// The commit a session started on, captured before any model turn so a later
187/// `git commit` cannot pass itself off as a clean tree.
188pub fn head_commit(dir: &std::path::Path) -> Option<String> {
189    let out = std::process::Command::new("git")
190        .current_dir(dir)
191        .args(["rev-parse", "HEAD"])
192        .output()
193        .ok()?;
194    out.status
195        .success()
196        .then(|| String::from_utf8_lossy(&out.stdout).trim().to_string())
197}
198
199/// Why a nomination was refused. Each variant is a distinct thing to tell the
200/// model, because "try again differently" and "you cannot get here from where
201/// you are" are different instructions.
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub enum NominationRefusal {
204    /// `summary` or `evidence` was empty, whitespace, or over the cap.
205    UnusableText(&'static str),
206    /// A mutating tool succeeded earlier in this session.
207    WorktreeWasMutated,
208    /// The worktree differs from the captured baseline right now.
209    WorktreeNotClean,
210    /// The baseline did not complete — starved by the session deadline, or a
211    /// check killed by infrastructure. A baseline that did not finish is not
212    /// evidence of anything.
213    BaselineIncomplete,
214}
215
216impl NominationRefusal {
217    /// The message handed back to the model as the tool result.
218    pub fn message(&self) -> String {
219        match self {
220            Self::UnusableText(which) => format!(
221                "report_no_change refused: `{which}` must be non-empty and under \
222                 {MAX_FINDING_TEXT} characters. State the conclusion and what you \
223                 examined to reach it."
224            ),
225            Self::WorktreeWasMutated => {
226                "report_no_change refused: this session already made a successful edit. \
227                 Reverting does not restore eligibility — a no-change finding is only \
228                 available to a session that never changed anything. Continue toward a \
229                 diff, or start a fresh session to investigate."
230                    .to_string()
231            }
232            Self::WorktreeNotClean => {
233                "report_no_change refused: the worktree differs from the baseline. \
234                 Restore it before concluding that nothing should change."
235                    .to_string()
236            }
237            Self::BaselineIncomplete => {
238                "report_no_change refused: the baseline evaluation did not complete, so \
239                 there is nothing to conclude from. This is not something you can fix — \
240                 the session needs more time or a working check environment."
241                    .to_string()
242            }
243        }
244    }
245}
246
247/// What the runtime decided to do with a nomination.
248#[derive(Debug, Clone, PartialEq, Eq)]
249pub enum NominationVerdict {
250    /// Terminal, with no human in the loop. The narrow path.
251    Autonomous,
252    /// Recorded, and parked for a human to accept or reject.
253    NeedsHuman,
254    /// Not recorded at all.
255    Refused(NominationRefusal),
256}
257
258/// Whether a baseline run actually finished.
259///
260/// An empty result set is not a clean baseline — it is a baseline that never
261/// ran. A check the *session clock* killed (`deadline_clamped` alongside
262/// `timed_out`) is not a verdict on the work either, which is the same
263/// distinction `code-task`'s event stream draws.
264pub fn baseline_completed(results: &[CheckResult]) -> bool {
265    !results.is_empty() && !results.iter().any(|r| r.timed_out && r.deadline_clamped)
266}
267
268/// The facts a nomination is judged against. All collected by the runtime; none
269/// asserted by the model.
270#[derive(Debug, Clone, Copy)]
271pub struct NominationContext<'a> {
272    pub kind: NoChangeKind,
273    pub summary: &'a str,
274    pub evidence: &'a str,
275    pub baseline: &'a [CheckResult],
276    pub provenance: ContractProvenance,
277    /// The worktree matches the captured baseline right now.
278    pub worktree_clean: bool,
279    /// A mutating tool has succeeded at some point this session.
280    pub mutated: bool,
281}
282
283/// Judge a nomination.
284///
285/// The ordering matters: the refusals that describe the *session's* history come
286/// before the ones about the finding's content, so a model that has already
287/// disqualified itself is told that rather than being invited to reword.
288pub fn evaluate_nomination(ctx: NominationContext<'_>) -> NominationVerdict {
289    use NominationRefusal::*;
290
291    if ctx.mutated {
292        return NominationVerdict::Refused(WorktreeWasMutated);
293    }
294    if !ctx.worktree_clean {
295        return NominationVerdict::Refused(WorktreeNotClean);
296    }
297    if !baseline_completed(ctx.baseline) {
298        return NominationVerdict::Refused(BaselineIncomplete);
299    }
300    if !usable(ctx.summary) {
301        return NominationVerdict::Refused(UnusableText("summary"));
302    }
303    if !usable(ctx.evidence) {
304        return NominationVerdict::Refused(UnusableText("evidence"));
305    }
306
307    // The one autonomous path. Every conjunct is load-bearing:
308    //   - PremiseWrong is the only shape a green baseline can speak to at all.
309    //   - baseline_gates_nothing means EVERY check passed untouched.
310    //   - is_trusted means the model did not author the checks it is citing.
311    // Drop any one and this becomes self-approval.
312    let autonomous = matches!(ctx.kind, NoChangeKind::PremiseWrong)
313        && baseline_gates_nothing(ctx.baseline)
314        && ctx.provenance.is_trusted();
315
316    if autonomous {
317        NominationVerdict::Autonomous
318    } else {
319        NominationVerdict::NeedsHuman
320    }
321}
322
323fn usable(text: &str) -> bool {
324    let trimmed = text.trim();
325    !trimmed.is_empty() && trimmed.len() <= MAX_FINDING_TEXT
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331
332    fn check(name: &str, passed: bool) -> CheckResult {
333        CheckResult {
334            credentials_allowed: false,
335            name: name.to_string(),
336            passed,
337            exit_code: Some(if passed { 0 } else { 1 }),
338            duration_ms: 1,
339            output_tail: String::new(),
340            timed_out: false,
341            deadline_clamped: false,
342        }
343    }
344
345    fn ctx<'a>(
346        kind: NoChangeKind,
347        baseline: &'a [CheckResult],
348        provenance: ContractProvenance,
349    ) -> NominationContext<'a> {
350        NominationContext {
351            kind,
352            summary: "the code already handles this",
353            evidence: "read handler.rs and ran the suite",
354            baseline,
355            provenance,
356            worktree_clean: true,
357            mutated: false,
358        }
359    }
360
361    #[test]
362    fn the_narrow_path_terminates_autonomously() {
363        let green = vec![check("a", true), check("b", true)];
364        assert_eq!(
365            evaluate_nomination(ctx(
366                NoChangeKind::PremiseWrong,
367                &green,
368                ContractProvenance::OperatorSupplied
369            )),
370            NominationVerdict::Autonomous
371        );
372    }
373
374    #[test]
375    fn a_model_authored_contract_never_self_approves() {
376        // The whole escape hatch, in one assertion: same green baseline, same
377        // kind, and the only difference is who wrote the checks.
378        let green = vec![check("a", true)];
379        assert_eq!(
380            evaluate_nomination(ctx(
381                NoChangeKind::PremiseWrong,
382                &green,
383                ContractProvenance::ModelDerived
384            )),
385            NominationVerdict::NeedsHuman
386        );
387    }
388
389    #[test]
390    fn a_runtime_generated_reproduction_is_trusted() {
391        // This is what lets autonomous self-triage work at all: the contract
392        // came from telemetry CAR collected before the session existed.
393        let green = vec![check("repro", true)];
394        assert_eq!(
395            evaluate_nomination(ctx(
396                NoChangeKind::PremiseWrong,
397                &green,
398                ContractProvenance::RuntimeGenerated
399            )),
400            NominationVerdict::Autonomous
401        );
402    }
403
404    #[test]
405    fn the_two_judgement_shapes_always_reach_a_human() {
406        let green = vec![check("a", true)];
407        for kind in [
408            NoChangeKind::DeliberateBehavior,
409            NoChangeKind::NonCodeDecision,
410        ] {
411            assert_eq!(
412                evaluate_nomination(ctx(kind, &green, ContractProvenance::OperatorSupplied)),
413                NominationVerdict::NeedsHuman,
414                "{kind:?} is not runtime-verifiable"
415            );
416        }
417    }
418
419    #[test]
420    fn a_red_baseline_proves_nothing_and_parks() {
421        let mixed = vec![check("a", true), check("b", false)];
422        assert_eq!(
423            evaluate_nomination(ctx(
424                NoChangeKind::PremiseWrong,
425                &mixed,
426                ContractProvenance::OperatorSupplied
427            )),
428            NominationVerdict::NeedsHuman
429        );
430    }
431
432    #[test]
433    fn editing_then_reverting_does_not_restore_eligibility() {
434        let green = vec![check("a", true)];
435        let mut c = ctx(
436            NoChangeKind::PremiseWrong,
437            &green,
438            ContractProvenance::OperatorSupplied,
439        );
440        // Worktree is clean again — the revert worked. The ledger still says no.
441        c.worktree_clean = true;
442        c.mutated = true;
443        assert_eq!(
444            evaluate_nomination(c),
445            NominationVerdict::Refused(NominationRefusal::WorktreeWasMutated)
446        );
447    }
448
449    #[test]
450    fn a_dirty_worktree_is_refused() {
451        let green = vec![check("a", true)];
452        let mut c = ctx(
453            NoChangeKind::PremiseWrong,
454            &green,
455            ContractProvenance::OperatorSupplied,
456        );
457        c.worktree_clean = false;
458        assert_eq!(
459            evaluate_nomination(c),
460            NominationVerdict::Refused(NominationRefusal::WorktreeNotClean)
461        );
462    }
463
464    #[test]
465    fn a_starved_baseline_is_not_evidence() {
466        let mut killed = check("a", false);
467        killed.timed_out = true;
468        killed.deadline_clamped = true;
469        let results = vec![killed];
470        assert_eq!(
471            evaluate_nomination(ctx(
472                NoChangeKind::PremiseWrong,
473                &results,
474                ContractProvenance::OperatorSupplied
475            )),
476            NominationVerdict::Refused(NominationRefusal::BaselineIncomplete)
477        );
478    }
479
480    #[test]
481    fn an_empty_baseline_is_not_a_clean_one() {
482        assert!(!baseline_completed(&[]));
483        assert_eq!(
484            evaluate_nomination(ctx(
485                NoChangeKind::PremiseWrong,
486                &[],
487                ContractProvenance::OperatorSupplied
488            )),
489            NominationVerdict::Refused(NominationRefusal::BaselineIncomplete)
490        );
491    }
492
493    #[test]
494    fn empty_or_oversized_text_is_refused() {
495        let green = vec![check("a", true)];
496        let mut c = ctx(
497            NoChangeKind::PremiseWrong,
498            &green,
499            ContractProvenance::OperatorSupplied,
500        );
501        c.summary = "   ";
502        assert_eq!(
503            evaluate_nomination(c),
504            NominationVerdict::Refused(NominationRefusal::UnusableText("summary"))
505        );
506
507        let huge = "x".repeat(MAX_FINDING_TEXT + 1);
508        let mut c2 = ctx(
509            NoChangeKind::PremiseWrong,
510            &green,
511            ContractProvenance::OperatorSupplied,
512        );
513        c2.evidence = &huge;
514        assert_eq!(
515            evaluate_nomination(c2),
516            NominationVerdict::Refused(NominationRefusal::UnusableText("evidence"))
517        );
518    }
519
520    #[test]
521    fn session_history_is_reported_before_content() {
522        // A disqualified session must be told it is disqualified, not invited to
523        // reword its way in.
524        let green = vec![check("a", true)];
525        let mut c = ctx(
526            NoChangeKind::PremiseWrong,
527            &green,
528            ContractProvenance::OperatorSupplied,
529        );
530        c.mutated = true;
531        c.summary = "";
532        assert_eq!(
533            evaluate_nomination(c),
534            NominationVerdict::Refused(NominationRefusal::WorktreeWasMutated)
535        );
536    }
537
538    /// Helper: a real git repo with one committed file.
539    fn git_repo() -> tempfile::TempDir {
540        let dir = tempfile::tempdir().unwrap();
541        let git = |args: &[&str]| {
542            std::process::Command::new("git")
543                .current_dir(dir.path())
544                .args(args)
545                .output()
546                .unwrap();
547        };
548        git(&["init", "-q"]);
549        git(&["config", "user.email", "t@t"]);
550        git(&["config", "user.name", "t"]);
551        std::fs::write(dir.path().join("f.txt"), "original\n").unwrap();
552        git(&["add", "-A"]);
553        git(&["commit", "-qm", "init"]);
554        dir
555    }
556
557    fn git(dir: &std::path::Path, args: &[&str]) {
558        std::process::Command::new("git")
559            .current_dir(dir)
560            .args(args)
561            .output()
562            .unwrap();
563    }
564
565    /// Committing an edit leaves `status` and `diff HEAD` both empty, so an
566    /// observation built only from those two reports a pristine tree over a
567    /// real, persistent change. Binding HEAD is what closes it.
568    ///
569    /// The only other thing standing against this is a line in the system
570    /// prompt asking the model not to commit — and under this feature's own
571    /// threat model, a prompt is not a control.
572    #[test]
573    fn a_committed_edit_is_not_a_pristine_tree() {
574        let dir = git_repo();
575        let start = head_commit(dir.path()).expect("HEAD readable");
576        let before = worktree_fingerprint(dir.path()).unwrap();
577        assert_eq!(worktree_is_pristine(dir.path(), &start), Some(true));
578
579        std::fs::write(dir.path().join("f.txt"), "mutated\n").unwrap();
580        git(dir.path(), &["add", "-A"]);
581        git(dir.path(), &["commit", "-qm", "sneak"]);
582
583        assert_ne!(
584            worktree_fingerprint(dir.path()).unwrap(),
585            before,
586            "a commit must move the fingerprint"
587        );
588        assert_eq!(
589            worktree_is_pristine(dir.path(), &start),
590            Some(false),
591            "HEAD moved, so the tree is not the one this session started on"
592        );
593    }
594
595    /// `git update-index --assume-unchanged` changes neither status nor diff,
596    /// and every later edit to that path is then invisible to both —
597    /// permanently, from metadata living in `.git/`. `ls-files -v` is the only
598    /// one of the four reads that sees it.
599    #[test]
600    fn an_assume_unchanged_flag_is_not_a_pristine_tree() {
601        let dir = git_repo();
602        let start = head_commit(dir.path()).expect("HEAD readable");
603        let before = worktree_fingerprint(dir.path()).unwrap();
604
605        git(dir.path(), &["update-index", "--assume-unchanged", "f.txt"]);
606
607        assert_ne!(
608            worktree_fingerprint(dir.path()).unwrap(),
609            before,
610            "setting the flag must itself register as a mutation"
611        );
612        assert_eq!(
613            worktree_is_pristine(dir.path(), &start),
614            Some(false),
615            "a tree that can hide later edits is not pristine"
616        );
617
618        // And the edit it was hiding stays invisible to status/diff — which is
619        // exactly why the flag itself has to be what disqualifies the session.
620        std::fs::write(dir.path().join("f.txt"), "tampered\n").unwrap();
621        assert_eq!(worktree_is_pristine(dir.path(), &start), Some(false));
622    }
623
624    #[test]
625    fn skip_worktree_is_caught_too() {
626        let dir = git_repo();
627        let start = head_commit(dir.path()).expect("HEAD readable");
628        git(dir.path(), &["update-index", "--skip-worktree", "f.txt"]);
629        assert_eq!(worktree_is_pristine(dir.path(), &start), Some(false));
630    }
631
632    #[test]
633    fn an_ordinary_edit_still_shows() {
634        let dir = git_repo();
635        let start = head_commit(dir.path()).expect("HEAD readable");
636        std::fs::write(dir.path().join("f.txt"), "edited\n").unwrap();
637        assert_eq!(worktree_is_pristine(dir.path(), &start), Some(false));
638    }
639
640    #[test]
641    fn a_head_that_does_not_match_is_never_pristine() {
642        let dir = git_repo();
643        assert_eq!(
644            worktree_is_pristine(dir.path(), "0000000000000000000000000000000000000000"),
645            Some(false)
646        );
647    }
648
649    #[test]
650    fn the_ledger_is_one_way() {
651        let ledger = MutationLedger::new();
652        assert!(!ledger.has_mutated());
653        ledger.record_mutation();
654        ledger.record_mutation();
655        assert!(ledger.has_mutated());
656    }
657
658    #[test]
659    fn trusted_provenance_has_exactly_one_untrusted_member() {
660        assert!(ContractProvenance::OperatorSupplied.is_trusted());
661        assert!(ContractProvenance::HumanConfirmed.is_trusted());
662        assert!(ContractProvenance::RuntimeGenerated.is_trusted());
663        assert!(
664            !ContractProvenance::ModelDerived.is_trusted(),
665            "a contract the session's own model wrote is never trusted — widening \
666             this is how the gate stops working"
667        );
668    }
669}