Interlock

Scenario gcp_resource: agent rollback vs. a human redeploy, Google Cloud Storage

Generated 2026-09-13T22:37:23+00:00 by experiments/scenario_gcp_resource.py. Model claude-haiku-4-5-20251001. Service: Google Cloud Storage JSON API (object versioning on), project gen-lang-client-0277439345, bucket interlock-sandbox-df15e7ea27 (deleted after the run).

fault want no_check hand_check interlock
crash_before_send_redeploy the human’s v43 stays live; the stale rollback never lands WRITTEN; 1 agent write(s) [‘1789338914659713’]; live generation 1789338914659713 deployed by agent; VIOLATED; answer matches GCS; record cannot say which writes landed; proof no; 2.0s PRECONDITION_FAILED; 0 agent write(s) []; live generation 1789338918615028 deployed by human-redeploy; held; answer matches GCS; record says 0 agent write(s); proof yes; 1.8s REFUSED:stale_premise_at_recovery; 0 agent write(s) []; live generation 1789338923362145 deployed by human-redeploy; held; answer matches GCS; record says 0 agent write(s); proof yes; 30.9s
crash_after_commit_redeploy the rollback landed once before the redeploy; the human’s v43 stays live WRITTEN; 2 agent write(s) [‘1789338955635377’, ‘1789338957530186’]; live generation 1789338957530186 deployed by agent; VIOLATED; answer matches GCS; record cannot say which writes landed; proof no; 1.9s ALREADY_APPLIED; 1 agent write(s) [‘1789338960381877’]; live generation 1789338961882231 deployed by human-redeploy; held; answer matches GCS; record says 1 agent write(s); proof yes; 2.1s COMMITTED_ON_QUERY; 1 agent write(s) [‘1789338965667223’]; live generation 1789338967121211 deployed by human-redeploy; held; answer matches GCS; record says 1 agent write(s); proof yes; 30.9s
crash_after_commit_no_change control: the rollback landed once and stays live; nothing writes it twice WRITTEN; 2 agent write(s) [‘1789338999316263’, ‘1789338999749322’]; live generation 1789338999749322 deployed by agent; VIOLATED; answer matches GCS; record cannot say which writes landed; proof no; 0.4s ALREADY_APPLIED; 1 agent write(s) [‘1789339002316691’]; live generation 1789339002316691 deployed by agent; held; answer matches GCS; record says 1 agent write(s); proof yes; 0.5s COMMITTED_ON_QUERY; 1 agent write(s) [‘1789339006019783’]; live generation 1789339006019783 deployed by agent; held; answer matches GCS; record says 1 agent write(s); proof yes; 30.6s

Verdict

On this scenario hand_check ties Interlock on what GCS holds (3/3 each), on the reported answer (3/3 each) and on the record (3/3 each), and Interlock is worse on time: about 31s from crash to settled against about 2s. no_check fails all three rows.

An earlier version of this suite gave Interlock a win in crash_after_commit_redeploy. That win came from a weak hand_check: on 412 it only compared the live object’s md5, so after “my rollback landed, then a human replaced it” it reported PRECONDITION_FAILED. This run’s hand_check does the cheap, idiomatic follow-up instead: it tags each upload with its queue job id and, on 412, lists the object’s versions for that id. That is about ten lines, the same lookup Interlock’s tier-2 query makes, and with it the win is gone.

Both lookups depend on the bucket’s object versioning. Without it, versions=true returns only the live object, so after a replaced write both hand_check (PRECONDITION_FAILED) and Interlock (REFUSED:stale_premise_at_recovery) would report the rollback as not landed when it did. That case is covered by offline tests (test_lookup_finds_own_write_after_a_redeploy_only_with_versioning, test_interlock_without_versioning_misses_its_replaced_write), not by a live run: without versioning GCS keeps no record of the replaced write, so there would be no service-side ground truth to judge against.

The scenario

Incident INC-2207: checkout returns 500s after config v42 was deployed to a GCS object. The on-call lead approves one change: roll the object back to v41. A real model (get_config, then rollback_config) reads the live object, including its generation, and decides the rollback. The worker then writes v41 and dies by SIGKILL at the crash point. During the outage a human redeploys a fix, v43, the way a person would (gcloud storage cp, no precondition). A new worker process restarts the job. Rolling back now would delete the human’s fix.

Ground truth is GCS itself: the bucket has object versioning on, so after the restart the harness lists every generation of the object and reads the live one. Every write carries custom metadata writer (human-deploy, agent, human-redeploy), so the version list says who wrote each generation.

Invariant per row: the number of agent writes equals the number wanted, and the live object was deployed by the party that should own it.

How each column is scored

The three systems

What this run shows

Interlock is worse on time in every row: a SIGKILLed sender’s claim (CLAIM_TTL = 30s here, above the 10s HTTP timeout) must expire before recovery may touch the effect, while hand_check relies on GCS’s atomic precondition and can resend at once. What Interlock adds here is not a different result but a different record: a hash-chained journal that also carries the incident (lease) check at send and at recovery, which hand_check’s log does not record. On this scenario that does not change any scored column.

Proof and artifacts

Limits

Ids