fix(effects): require evidence for external completion claims

Effect obligations were consulted only for declared artifacts, and reported
external success could be presented as done. Now, regardless of declared
artifacts:

- the latest effect on any changed file contradicted by a fresh readback
  fails the run (a superseded earlier effect is history, not a contradiction);
- a passing verifier followed by an effect that may have changed state
  without settled evidence is stale (BLOCKED);
- executed external effects that are not VERIFIED cap the decision at
  UNVERIFIED, and the answer always carries server-authored facts for them
  ("reported success; any external change it made was not independently
  verified", "reported failure", "unknown outcome").

The disclosure is structural and does not depend on recognizing the model's
wording. When it is the only change, the model's answer events are released
unchanged and the disclosure follows as one delta (and in round_texts).
Prose filtering is also tightened (remote verbs are mutation claims, an
unnamed "I updated it" cannot borrow the single required artifact, bare
"Done." is a terminal claim beside unverified external effects). A passing
verifier still supports test claims; it never speaks for the external effect.

Replaces the uncommitted attempt that blocked every run with any RUNNING
effect: a background launch with no declared obligations completes
UNVERIFIED.
This commit is contained in:
Alexandre Teixeira
2026-10-03 00:58:32 +01:00
parent 682b44a3ec
commit b23c6d40b3
4 changed files with 153 additions and 27 deletions
+2
View File
@@ -1228,6 +1228,8 @@ def test_native_host_shell_call_runs_through_bridge_and_threads_result(monkeypat
assert host_output["call_id"] == "call_host_1"
assert host_output["tool_call_id"] == "call_host_1"
assert any("ajax is at 192.168.1.42" in event.get("delta", "") for event in events)
# The host bridge is an external effect: its disclosure follows the answer.
assert any("External operation host_shell reported success" in event.get("delta", "") for event in events)
def test_workspace_agents_md_lands_in_untrusted_prompt_message(tmp_path, monkeypatch):