Skip to content

fix(cli): the report predicted a tier the run had already decided - #345

Merged
AminChirazi merged 1 commit into
mainfrom
claude/report-achieved-tier
Aug 1, 2026
Merged

fix(cli): the report predicted a tier the run had already decided#345
AminChirazi merged 1 commit into
mainfrom
claude/report-achieved-tier

Conversation

@AminChirazi

Copy link
Copy Markdown
Contributor

The behaviour half of the defect #344 documented.

#341 made the run's containment tier win over the probe's, and check_egress has honoured that since. The report did not. It called containment(spec) before the run started, printed that, and reused it for the --json payload.

On Linux the two agree by construction, so this was invisible. On Windows the run is the authority — seven steps can fail after the probe passes — so a run that was contained printed not contained beside a trace lane saying the opposite.

That is worse than either being wrong alone. An auditor holding two artifacts that describe one run differently has no way to tell which is lying, and the artifact that is right is the one nobody printed.

The shape

record and replay now return (Containment, Result<(), String>) — the tier they achieved, alongside their outcome.

The tier leaves by out-parameter into a thin wrapper rather than through the return type, because it becomes known part way through the body: every ? before that point would otherwise have to name a tier it does not have yet. A run that failed before it started reports the prediction, which is the honest answer for a run that achieved nothing. That is also why it is a tuple rather than Result<Containment, _> — a failed run still has a tier worth reporting.

achieved_tier is one function, used by the certification path and the reporting path alike. Those two computing it separately is how they came to disagree in the first place.

The corrected line prints only when the run's answer differs from the prediction, so nothing changes on Linux and no output grows a line for a distinction that platform does not have. --json always carries the achieved tier.

Verification

  • cargo test --workspace --all-features — 728 tests green
  • cargo fmt --check — clean
  • ratchets — diff size 107 <= 400, tests 773 -> 775, nothing silenced
  • the_reported_tier_is_the_one_the_run_achieved was mutation-checked: making achieved_tier ignore the run and return the prediction fails it

Both directions are asserted, and the second is not implied by the first: where the run decided no tier — a url: service, a flow engaging no egress, a platform with no mechanism — the prediction stands. A run that determined nothing must not read as one that achieved nothing; those are different sentences.

Not fixed here, and it is the same defect

The run record still stores the predicted tier. build_control_record is called where the run is not in scope, so threading it there is a larger change than this one and would bury the fix above. Flagging it rather than leaving it to be rediscovered.


Generated by Claude Code

#341 made the run's containment tier win over the probe's, and
`check_egress` has honoured that since. The REPORT did not. It called
`containment(spec)` before the run started and printed that, then reused
it for the `--json` payload.

On Linux the two agree by construction, so this was invisible. On
Windows the run is the authority — seven steps can fail after the probe
passes — so a run that WAS contained printed "not contained" beside a
trace lane saying the opposite.

That is worse than either being wrong on its own. An auditor holding two
artifacts that describe one run differently has no way to tell which is
lying, and the artifact that is right is the one nobody printed.

So `record` and `replay` return the tier they achieved alongside their
outcome, and the report prefers it. The tier leaves by out-parameter
because it becomes known part way through: every `?` before that point
would otherwise have to name a tier it does not have yet. A run that
failed before it started reports the prediction, which is the honest
answer for a run that achieved nothing.

`achieved_tier` is one function used by both paths. The certification
path and the reporting path computing this separately is how they came
to disagree in the first place.

The corrected line prints only when the run's answer DIFFERS from the
prediction, so nothing changes on Linux and no output grows a line for a
distinction that platform does not have.

Not fixed here, and it is the same defect: the run record at
`build_control_record` still stores the predicted tier. It is built
where the run is not in scope, which is a larger change than this one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XdXrbksFKirm7yW6EDunur
Copilot AI review requested due to automatic review settings August 1, 2026 13:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes the CLI’s containment reporting for app: agent flows so stdout and --json reflect the tier actually achieved by the run (when the run determines one), rather than only the pre-run probe prediction. This aligns reporting with the certification/verdict path (check_egress) and prevents a single run producing conflicting artifacts.

Changes:

  • Change agent_flow::record / agent_flow::replay to return (Containment, Result<(), String>), carrying the achieved tier alongside the run outcome.
  • Introduce achieved_tier(run, spec_tier) as the shared rule for “report what the run achieved, otherwise fall back to the prediction”.
  • Update CLI command paths to print the predicted tier first, and reprint only if the run’s achieved tier differs; --json always includes the achieved tier.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
crates/flowproof-cli/src/lib.rs Updates record/run/suite agent-flow paths to print/report the achieved containment tier (with minimal extra stdout).
crates/flowproof-cli/src/agent_flow.rs Adds achieved-tier computation and changes record/replay APIs to return achieved tier + outcome; adds regression tests.
Suppressed comments (2)

crates/flowproof-cli/src/lib.rs:721

  • Same as the single-run path: predicted is computed here for printing, but agent_flow::replay also computes containment(spec) internally, duplicating host probes and making the tier != predicted comparison depend on two independently constructed values. Consider plumbing the precomputed predicted into replay so the suite path and replay logic stay strictly consistent.
    let predicted = agent_flow::containment(spec);
    if !json {
        println!("{}", predicted.report_line());
    }
    let (tier, outcome) = agent_flow::replay(spec, trace_path);
    if !json && tier != predicted {

crates/flowproof-cli/src/lib.rs:1303

  • This path computes predicted via containment(&spec) and then calls agent_flow::replay, which recomputes containment(spec) internally. That duplicates the Windows host probe and makes tier != predicted compare values built by different probe calls. Consider passing the already-computed predicted into replay to avoid redundant probing and guarantee the comparison reflects only run-vs-probe differences.
        let predicted = agent_flow::containment(&spec);
        if !json {
            println!("{}", predicted.report_line());
        }
        let (tier, outcome) = agent_flow::replay(&spec, &trace_path);
        if !json && tier != predicted {

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +843 to +845
pub fn achieved_tier(run: &AgentRun, spec_tier: &Containment) -> Containment {
run.containment.clone().unwrap_or_else(|| spec_tier.clone())
}
Comment on lines +301 to +305
let predicted = agent_flow::containment(&spec);
if !json {
println!("{}", predicted.report_line());
}
let (tier, outcome) = agent_flow::record(&spec, &out);
@AminChirazi
AminChirazi merged commit 011044b into main Aug 1, 2026
10 checks passed
AminChirazi added a commit that referenced this pull request Aug 1, 2026
The last of the three places that read a predicted tier instead of the
achieved one. #345 fixed the printed report and the JSON; the run record
— the artifact an auditor actually keeps — still called
`agent_flow::containment(spec)` for itself.

This one costs more than a wrong label. `containment` is read twice in
`build_control_record`: once as the record's own tier line, and once to
decide whether the blocked lane is evidence at all. So the two tiers
disagreeing decides whether the destinations a run refused are carried
or discarded — in either direction. An optimistic probe over a run that
was not contained would present destinations nothing on this run
blocked; a pessimistic one over a run that was contained would throw
away the only proof it produced.

So the achieved tier is passed in, `None` for a step-engine flow, which
has no agent run to ask.

The test asserts the PESSIMISTIC direction, and that is a deliberate
retreat. The interesting production case is the opposite one — a Windows
run that was contained, over a probe predicting otherwise — and it
cannot be made falsifiable here. This suite runs on Linux, where the
probe already answers Enforced, so a test asserting "the achieved
Enforced won" passes identically when the achieved tier is ignored
altogether. It was written that way first and it survived the mutation,
which is the definition of a test that proves nothing.

Turned around it pins the half that can fail here, and it is the
safety-critical half: an uncontained run must not inherit an optimistic
probe's evidence. Mutation-checked in that direction.

The fixture guard on the blocked lane earned its keep again — the first
draft wrote the cassette in the wrong shape, the lane failed to parse,
and without the guard the emptiness assertion would have passed for the
wrong reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XdXrbksFKirm7yW6EDunur
AminChirazi added a commit that referenced this pull request Aug 1, 2026
Records #345 and #347, which are user-visible and unreleased. The
Windows containment work they came out of is deliberately NOT written up
here: no Windows job has been confirmed to run its end-to-end test, and
an entry claiming a mechanism nobody has watched work is the kind of
sentence this file exists not to contain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XdXrbksFKirm7yW6EDunur
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants