Claims you can run#
In stock Terraform and OpenTofu, the state file is the record of what you own. Everything defends that file: backends store it and locks serialize access to it, and if you lose it the tool no longer knows your infrastructure exists. Choudoufu moves the record onto the platform itself - identity as tags on each resource, values in a record store, effects as receipts - and demotes the state file to a disposable cache.
That design implies testable promises. Each one is a smoke
scenario: Docker plus a local AWS emulator, one to three minutes each;
exit 0 means every assertion held. Each scenario can also run inverted. Under
BREAK=1 it manufactures the exact corruption the claim guards against
and passes only by catching it. A test that cannot
fail proves nothing, so every claim ships with its failure demonstrated.
Claim 15 inverts the control rather than dropping it: its risk is a
refusal that fires unconditionally, so its BREAK=1 run removes the
fault and requires the run to succeed. Claim 20 is the one claim on this
page with no scenario and no BREAK=1 control: it cites measurements
already published elsewhere in this repository rather than proving
itself fresh, and it says so rather than reading like the other
nineteen.
| Claim | Scenario | ~time |
|---|---|---|
| Owned resources cannot fall out of plans unnoticed | just smoke no-silent-orphans | 2 min |
| Contention settles at the platform API, never in a lock | just smoke no-self-managed-locks | 2 min |
| Staleness costs reads, never results | just smoke staleness-costs-reads | 3 min |
| Declaring the backend is the whole setup | just smoke backend-sets-itself-up | 1 min |
| Recovery is a re-run, never surgery | just smoke recovery-is-a-rerun | 2 min |
| The roundtrip: one command in, one file out | just smoke roundtrip | 3 min |
| Identity is a tag you can read and move | just smoke identity-is-a-tag | 3 min |
| Stock when you need it | just smoke stock-when-you-need-it | 3 min |
| Unchanged is free | just smoke unchanged-is-free | 3 min |
| The cache serves the whole estate | just smoke cache-serves-the-whole-estate | 2 min |
| A count pool is a fungible set | just smoke count-is-a-fungible-set | 2 min |
| Carve by retag | just smoke carve-by-retag (needs Go) | 6 min |
| The tag is the boundary | just smoke the-tag-is-the-boundary | 4 min |
| A plan costs its estate, not its account | just smoke plan-cost-tracks-the-estate | 2 min |
| Apply exactly what was approved | just smoke apply-what-was-approved | 4 min |
| The boundary holds across provider configurations | just smoke the-boundary-holds-across-regions | 2 min |
| A record-only composite identity survives cache loss without a duplicate create | just smoke record-only-survives-cache-loss | 2 min |
| A replaced object’s shadow is not a second claimant | just smoke a-shadow-is-not-a-claimant | 3 min |
| The boundary holds across accounts | just smoke the-boundary-holds-across-accounts | 2 min |
| Scale, cited rather than re-run | (no scenario - evidence-cited, see claim 20) | — |
Claim 1: owned resources cannot fall out of plans unnoticed#
When an apply crashes after the create call but before the write to state, stock tooling orphans the resource: it exists and it bills, but no plan will ever mention it again. Here the plan reads identity from the resource’s own tags, so a resource nobody remembers still walks into the next plan by name.
Clone https://github.com/INTENTIUS/choudoufu. Confirm Docker is running
(docker info) and the AWS CLI is installed. If Go is not installed,
export CHOUDOUFU_VERSION=<latest tag from
https://github.com/INTENTIUS/choudoufu/releases>. From the repo root run:
just smoke no-silent-orphans
Explain each step's verdict line to me as it prints. Then run
BREAK=1 just smoke no-silent-orphans and report the "caught" line: the
scenario creates the one shape the claim excludes and must fail to
claim it.The steps, in the order they print:
stand the estate up- an apply builds a small VPC estate; every create call carries the two identity tags, estate and address.the crash shape- a subnet is created the way a crashed apply leaves one: real resource, tags written, recorded nowhere. Stock tooling can never see this subnet again.the next plan finds it- the forgotten subnet appears as a named plan line. Nobody re-imported it and no file remembered it; the tags did.a deleted block is the same story- a resource removed from the configuration surfaces as a destroy the same way, through the same read.applying removes them - loudly, exactly- the plan proposes exactly two destroys and the apply performs exactly two.where the machinery does not reach, it says so out loud- two of the estate’s types sit outside the sweep today, and the apply names them and the consequence up front. Degrading to a warning is allowed; silence is not.the same claim where values live in the record store- aterraform_dataresource has no cloud presence to tag, so its record lives in the record store; delete its block and it surfaces from the store’s own list. No state file or cloud is involved.teardown- the estate is destroyed to an exact count.
The BREAK=1 run creates the subnet without identity tags. That is
the one shape the claim excludes, so the scenario must refuse to claim
it.
Claim 2: contention settles at the platform API, never in a lock#
Stock backends take a lock before touching state, because two writers
corrupting one file is fatal when the file is the record. A stuck lock
then needs force-unlock. With no authoritative file to defend there is no lock at all; two
racing applies are refereed by the platform’s own uniqueness rules.
Clone https://github.com/INTENTIUS/choudoufu. Confirm Docker is running
(docker info) and the AWS CLI is installed. If Go is not installed,
export CHOUDOUFU_VERSION=<latest tag from
https://github.com/INTENTIUS/choudoufu/releases>. From the repo root run:
just smoke no-self-managed-locks
Explain each step's verdict line to me as it prints. Then run
BREAK=1 just smoke no-self-managed-locks and report the "caught" line:
it strips the race winner's identity marker and convergence must fail.Step by step:
there is no lock to force open, and the tool says so-force-unlockrefuses with the true reason instead of pretending a lock exists.the race- two applies of the same client-named IAM role start at the same moment. The cloud’s name-uniqueness constraint referees; the phrase “Acquiring state lock” appears in neither output.the loser converges by reading reality- the losing apply’s next plan isNo changes.Its whole recovery is one ordinary plan.the one race the API cannot referee is a named collision- server-assigned resources can genuinely duplicate; the duplicate surfaces as a named pair rather than hiding.the human resolves it- one delete, and the estate is clean again.teardown.
Claim 3: staleness costs reads, never results#
A stale state file is the classic failure: the file is the record, so its lies become your plans. Here the file is a cache, never consulted for ownership; live reads win every disagreement. Losing or corrupting it costs a slower run and nothing else.
Clone https://github.com/INTENTIUS/choudoufu. Confirm Docker is running
(docker info) and the AWS CLI is installed. If Go is not installed,
export CHOUDOUFU_VERSION=<latest tag from
https://github.com/INTENTIUS/choudoufu/releases>. From the repo root run:
just smoke staleness-costs-reads
Explain each step's verdict line to me as it prints. Then run
BREAK=1 just smoke staleness-costs-reads and report the "caught" line:
it moves the live world mid-comparison and the equality check must
notice.In print order:
manufacture a genuinely ancient cache- apply, save the cache aside, destroy the whole estate, apply again. The saved cache now remembers only dead ids; the run proves the old and new VPC ids differ.three cache states, one answer- the same plan runs against the fresh cache, then the ancient one, then no cache file at all. The outputs are byte-identical.the world moves and the fresh cache does not hide it- a setting is changed behind the tool’s back with the AWS CLI; the next plan shows the drift straight through a fresh cache, and the apply reconverges it.the one opt-in, and where the cost actually lives--refresh=falseis the single path that serves reads from cache, and only for instances the sweep has already verified. The run measures its cache hits, then reruns with the cache gone to show none. The two outputs prove equal and both request counts print side by side. The price of staleness is paid in work, never in answers.the same answer where values live in the record store- the same ancient-cache trick against the record store, plus a phantom: the cache remembers a resource that no longer exists anywhere. The plan neither destroys the phantom nor misses the survivor.teardown.
Claim 4: declaring the backend is the whole setup#
Stock remote state has a day one: create a bucket, enable versioning,
create a lock table, write IAM for both, run init, answer migration
prompts, keep it all in step forever. Here the backend’s stores
provision themselves at first use, and each proves its own read path
with a sentinel before any plan trusts it.
Clone https://github.com/INTENTIUS/choudoufu. Confirm Docker is running
(docker info) and the AWS CLI is installed. If Go is not installed,
export CHOUDOUFU_VERSION=<latest tag from
https://github.com/INTENTIUS/choudoufu/releases>. From the repo root run:
just smoke backend-sets-itself-up
Explain each step's verdict line to me as it prints. Then run
BREAK=1 just smoke backend-sets-itself-up and report the "caught" line:
it makes the store unreachable and the run must refuse by name rather
than plan an empty-looking estate.As the run prints them:
no store declared - the local one appears unbidden- a live block with nothing about storage gets a.tofu-recordsdirectory beside the module at first use, sentinel already written. Zero setup steps.it works: the effect survives between runs- the recorded resource survives a replan, so the store is real, not scaffolding.a cloud store is one declaration, and it provisions itself-record_store "ssm" {}is the entire cloud setup; the store writes its sentinel into Parameter Store and the AWS CLI reads it back.teardown- nothing to deprovision, because nothing was ever provisioned by hand.
The BREAK=1 run makes only the SSM store unreachable while the
provider stays healthy. A store that cannot answer must refuse loudly,
naming itself, because a store that answers with silence would read as
an empty estate and the next plan would propose rebuilding everything.
Claim 5: recovery is a re-run, never surgery#
Two disasters end an estate’s day under stock. An apply that crashes after a create call leaves a resource no state file knows about; re-applying creates a duplicate and the original leaks. A lost state file is worse, because the file was the record of everything you own. Both end the same way here: run it again.
Clone https://github.com/INTENTIUS/choudoufu. Confirm Docker is running
(docker info) and the AWS CLI is installed. If Go is not installed,
export CHOUDOUFU_VERSION=<latest tag from
https://github.com/INTENTIUS/choudoufu/releases>. From the repo root run:
just smoke recovery-is-a-rerun
Explain each step's verdict line to me as it prints. Then run
BREAK=1 just smoke recovery-is-a-rerun and report the "caught" line: it
withholds the crashed resource's markers and the bind check must fail
rather than claim an unmarked resource.The steps as they print:
the crash - an apply dies after its first create- the VPC is made with the AWS CLI and stamped with the estate’s markers, exactly what the dead apply would have written before crashing. The configuration still declares it.re-run the apply - it binds, completes, duplicates nothing- the whole recovery is the same apply again: it findsaws_vpc.mainalready owned and builds the rest around it; the vpc keeps its id and the follow-up plan is clean.now lose every local file- the cache and the whole.terraformdirectory are deleted; after an init, the next plan isNo changes.The narration also says what the deleted cache held, and why that disposable file is the one place allowed to hold it.teardown- the crashed vpc is destroyed with the rest of the estate. It was a full citizen from the moment it was bound.
The BREAK=1 run withholds the markers. The estate must refuse to bind
an unmarked resource, so the re-run builds a second vpc - stock’s crash
behavior, demonstrated as the exact boundary of the claim.
Claim 6: the roundtrip - one command in, one file out#
Migrating to a new state tool is usually a trapdoor: once your estate
is in, the only way back is another migration project. Here the door in
is live-import, which reads the state file you already have and
stamps ownership markers on what verifies, and the door out is the
local cache, which is a stock-format state file ready to hand back.
Clone https://github.com/INTENTIUS/choudoufu. Confirm Docker is running
(docker info) and the AWS CLI is installed. If Go is not installed,
export CHOUDOUFU_VERSION=<latest tag from
https://github.com/INTENTIUS/choudoufu/releases>. From the repo root run:
just smoke roundtrip
Explain each step's verdict line to me as it prints. Then run
BREAK=1 just smoke roundtrip and report the "caught" line: it skips
live-import, and the plan must propose a duplicate estate - the
documented quiet failure of every migration.The steps as they print:
stock stands the estate up, tagless- pinned stock OpenTofu, in its own container, applies a seven-resource estate with no tags anywhere. Identity exists only interraform.tfstate.the door in - one command-live-importreads that file once and verifies every resource against the live system; what verifies gets the two markers. Nothing else is touched.bound - and the old record is now optional- the choudoufu plan is clean, the state file is deleted, and an ordinary apply keeps the estate converged while refreshing the cache.the door out - one file- the cache is copied toterraform.tfstateand the live block removed. Stock’s first plan back proposes only the removal of the two marker tags; one apply later, nothing of the fork remains.teardown - by stock, from the handed-back file- stock destroys all seven resources using the file choudoufu handed back.
The BREAK=1 run skips the one command. The plan must then propose
building a duplicate estate beside the real one, because turning on the
live block never binds resources by itself - the markers do.
Claim 7: identity is a tag you can read and move#
Because ownership lives on each resource as two tags, three things
follow that stock cannot offer: estates in one account are isolated by
construction, any AWS tool can answer ownership without this tool
present, and renaming a resource in code is a tag rewrite where stock
demands state mv surgery.
Clone https://github.com/INTENTIUS/choudoufu. Confirm Docker is running
(docker info) and the AWS CLI is installed. If Go is not installed,
export CHOUDOUFU_VERSION=<latest tag from
https://github.com/INTENTIUS/choudoufu/releases>. From the repo root run:
just smoke identity-is-a-tag
Explain each step's verdict line to me as it prints. Then run
BREAK=1 just smoke identity-is-a-tag and report the "caught" line: it
renames the resource in code but skips the retag, and the plan must
propose the destroy-and-recreate stock would inflict.The steps as they print:
two estates stand up in one account- two copies of the estate, different estate tags, one account. Nothing else separates them.any AWS tool answers ownership- the plain CLI’s tagging API lists each estate’s resources and reads a resource’s address tag. No choudoufu involved.neither estate can see the other- both plans are clean, and neither plan output ever names the other estate’s resources.a rename is a retag, not surgery-aws_vpc.mainbecomesaws_vpc.corein code,live-mvrewrites the address tag on the live resource, and the next plan is clean. No state file was edited, because there is none to edit.teardown - both estates, each by its own destroy.
The BREAK=1 run skips live-mv after the code rename. The live vpc
still wears the old address, so the plan must treat the new name as
missing and the old one as orphaned - stock’s destroy-and-recreate,
demonstrated as what the retag saves you from.
Claim 8: stock when you need it#
Stock behavior is not a mode you leave behind - it is the fallback, whole and exact, one deleted live block away. The scenario measures that rather than promising it: choudoufu and the pinned stock oracle plan the same state-backed estate side by side with debug logging on, and the plan texts match and so do the request counts, exactly. And with the live backend on, what you pay scales with your estate rather than the account around it.
Clone https://github.com/INTENTIUS/choudoufu. Confirm Docker is running
(docker info) and the AWS CLI is installed. If Go is not installed,
export CHOUDOUFU_VERSION=<latest tag from
https://github.com/INTENTIUS/choudoufu/releases>. From the repo root run:
just smoke stock-when-you-need-it
Explain each step's verdict line to me as it prints. Then run
BREAK=1 just smoke stock-when-you-need-it and report the
"caught" line: it runs the choudoufu leg with the live block ON, and
the measurement must show the difference.The steps as they print:
a stock estate, stood up by choudoufu with no live block- the fixture’s live block is removed and choudoufu applies the ordinary way: a realterraform.tfstate, no markers, no hooks.same plan, same requests- choudoufu and the pinned oracle each plan the estate withTF_LOG=debug; the scenario asserts the filtered plan texts are equal and the request counts identical. This is the #588 parity measurement as a two-minute demo.the live backend on - and you pay for your estate, not your account- the live estate goes up and its plan’s request count is measured. Twenty foreign resources then appear in the account and the count is measured again; it must not move.teardown- estate and clutter both removed.
The BREAK=1 run plans the choudoufu leg with the live block on. The
asked-for machinery must show up in the measurement - a live plan that
measured identical to stock would mean the parity comparison compares
nothing.
Claim 9: unchanged is free#
Re-planning an estate that did not change should not cost a full
re-read of it, and here it does not. On the -refresh=false path, an
instance the run can vouch for is served from the state cache and its
wire reads are never made - the bill is measured live, in the run’s own
debug stream. The whole pass answers to one estate-level argument:
reads = "full" in the live block turns it off (CHOUDOUFU_READS
overrides per run), and turning it off may change the price but never
the plan. For record-backed resources the attestation is the record
itself, on every default plan, with nothing opted into.
Clone https://github.com/INTENTIUS/choudoufu. Confirm Docker is running
(docker info) and the AWS CLI is installed. If Go is not installed,
export CHOUDOUFU_VERSION=<latest tag from
https://github.com/INTENTIUS/choudoufu/releases>. From the repo root run:
just smoke unchanged-is-free
Explain each step's verdict line to me as it prints. Then run
BREAK=1 just smoke unchanged-is-free and report the "caught" line: it
overwrites a record with garbage, and the run must refuse by name
rather than plan against made-up values.The steps as they print:
stand the estate up- an estate and a fresh cache, nothing changed since.the free re-plan, and the argument that refuses it- the same-refresh=falseplan runs under the default policy and again underCHOUDOUFU_READS=full. Selective serves the vouched instances and the request count drops; full serves nothing and pays every read; the two outputs must not differ by a byte. The toggle prices the plan, never changes it.teardown the cloud estate.the record-backed half - the record is the attestation- aterraform_data’s record is edited behind the tool’s back, and the next default plan surfaces the named reconvergence (~ input). The record is not a cache of the values; it is the values.
The BREAK=1 run overwrites the record with garbage. The run must fail
with the record refusal, naming the exact address - a store that cannot
answer never improvises. Default plans are untouched by all of this:
they read fully under either policy, because the read is drift
detection (claim 3 pins that forever).
Claim 10: the cache serves the whole estate#
-refresh=false is the path that serves from the state cache instead of
reading live. Until now it served the schema-admitted and record-backed
slice; a converged estate’s server-assigned resources - VPCs, subnets,
security groups, every id the cloud hands out - were read live anyway.
Now they are served too, so one estate of a decomposed terralith plans
at the speed of reading a file rather than re-reading the cloud. A
default plan still refreshes, because the read is drift detection, and
the serving is vouched by the estate sweep, so a deleted resource is
caught rather than served from cache.
Clone https://github.com/INTENTIUS/choudoufu. Confirm Docker is running
(docker info) and the AWS CLI is installed. If Go is not installed,
export CHOUDOUFU_VERSION=<latest tag from
https://github.com/INTENTIUS/choudoufu/releases>. From the repo root run:
just smoke cache-serves-the-whole-estate
Explain each step's verdict line to me as it prints. Then run
BREAK=1 just smoke cache-serves-the-whole-estate and report the "caught"
line: it deletes a resource out of band, and the plan must surface it
rather than serve the gone object from cache.The steps as they print:
stand the estate up- four server-assigned resources with a warm cache.a default plan reads them all- zero served; a default plan refreshes for drift by ruling.-refresh=false serves every instance from cache- all four served, the estate planned without re-reading a resource.serving is existence-vouched- the estate sweep confirms each is still live before serving, so a deletion is caught.
The BREAK=1 run deletes a resource out of band. The sweep no longer
vouches it, so it is not served from cache and the plan surfaces it -
losing an object costs a read, never a wrong plan.
Claim 11: a count pool is a fungible set#
A count block declares a set, and stock tools treat it as a list:
instance 2 is whatever sits at index 2. Shrinking the count renumbers
the tail and rebuilds it. Where the members are genuinely interchangeable
- nothing in the configuration says which live resource is which -
choudoufu names each one with a
tofu-slotmarker instead, a stable id minted once and never reused. The lint boundary admitscount.indexin an identity-bearing argument only where it can prove every instance renders a distinct value, and a block that does name its members that way needs no slot: the configuration already says which is which. For the fungible kind, the index is where a member sits today; the slot is what it is. So a pool of three scales to two by removing exactly one member and rebuilding nothing, and the survivors keep their live ids. Strip the slot from one member where no local record names it, and the set has two rules for naming its members, so the run refuses rather than guess.
The other kind of count block is the one whose members the
configuration itself names - a log group whose name is built from
count.index. There, the live resource that is instance k is the one the
configuration names, nothing is left for a slot to decide, and none is
written: those members carry tofu-estate and tofu-address and the
index in the address is what says which instance each one is. A reader
never has to consult a configuration to tell the two kinds apart, because
it can read the set: slots present, bind by slot; slots absent, bind by
tofu-address. That correct absence is what an operator reported as a
bug in issue #969, and the reason it read as one is that no claim step
had ever read the tag set back off a member of the second kind. Step 5
does, on both kinds at once.
Clone https://github.com/INTENTIUS/choudoufu. Confirm Docker is running
(docker info) and the AWS CLI is installed. If Go is not installed,
export CHOUDOUFU_VERSION=<latest tag from
https://github.com/INTENTIUS/choudoufu/releases>. From the repo root run:
just smoke count-is-a-fungible-set
Explain each step's verdict line to me as it prints. Then run
BREAK=1 just smoke count-is-a-fungible-set and report the "caught"
line: it deletes the local record, strips the slot marker from one
member, and the plan must refuse the half-slotted set by name rather
than bind the odd member by a guess. Then run
BREAK_SLOT=1 just smoke count-is-a-fungible-set and report its "caught"
line too: that one stamps a tofu-slot onto a member the configuration
names, where none belongs, and the same tag read that passes in the
ordinary run must fail on it.The steps as they print:
stand up a pool of three- three elastic IPs, three distinct slots.capture the survivor at the middle seat- the allocation id that holds slot 1 is written down.scale to two - one removed, nothing rebuilt- count drops to 2; the plan shows one destroy and zero creates, then applies.the middle survivor is the same live object- the id from step 2 is still allocated. Its seat moved and its identity did not.both kinds of count instance, read back with the plain AWS CLI- twocountblocks of one type,aws_cloudwatch_log_group, differing in exactly one property: one names its members (name = "/svc/${count.index}"), the other leaves the name to the provider (name_prefix). Every tag is read back off the live log groups with the plain AWS CLI, no choudoufu in the read, and compared as a whole key set. The named pair carries exactlypurpose,tofu-addressandtofu-estate, with the estate and the address asserted by value and notofu-slot; thename_prefixpair carries exactlytofu-address,tofu-estateandtofu-slot, with the slots0and1read off the live groups. Then the next plan is empty, so the first pair bound by its addresses and the second by its slots. One type, onelist-tags-for-resourcecall, one apply: a slot coming back missing on one pair cannot be a broken query when the identical query answers on the pair beside it.teardown- the pool is destroyed.
This claim carries two BREAK controls, because step 5 asserts a
presence and an absence and no single corruption tests both.
The BREAK=1 run deletes the local files, cache and record store both,
so nothing but the tags names a member, then deletes the tofu-slot tag
from one of them. Two members now answer by slot and one has no answer,
so the plan refuses the half-slotted set and names the slot disagreement
rather than binding the odd member by position. Beside an intact record
the same strip is a repair, not a guess: the record names the member and
the plan re-stamps its slot.
The BREAK_SLOT=1 run is that control’s mirror. Step 5’s claim about the
named pair is that a tag is NOT there, so the only corruption that can
test it is a tag that should not be there: it stamps tofu-slot=0 onto
/svc/0 with the AWS CLI and then runs the same check the ordinary run
runs - the same function, not a copy of it - which must fail, naming the
key set it read.
Claim 12: carve by retag#
In stock tooling every ownership boundary is a state file, so splitting a
monolith into team estates is state surgery. Each resource is moved
between files by hand, and for a moment it sits in two ledgers or in
none. Here the boundary is
a tag. A resource leaves one estate for another by having its
tofu-estate tag rewritten. The tool refuses a write that would leave
either side dirty, and afterwards each side plans clean and pays only
for what it holds.
Clone https://github.com/INTENTIUS/choudoufu. Confirm Docker is running
(docker info), the AWS CLI is installed, and Go is installed (this
scenario generates its estate with go run). From the repo root run:
just smoke carve-by-retag
Explain each step's verdict line to me as it prints. Then run
BREAK=1 just smoke carve-by-retag and report the "caught" line: it moves
the six blocks but skips the retag, and the monolith must propose
destroying the leavers while the new estate proposes building them.The steps as they print:
stock stands the terralith up- the pinned stock OpenTofu applies the generated terralith the ordinary way. It is 79 resources. Most of them are IAM. A small ECS layer and a Route 53 fan-out sit beside them. The estate carriescountandfor_eachexpansion and one module-nested pod. One state file, and not a marker anywhere.one command adopts it, and the state file is deleted-live-importreads the file once and stamps 38 resources; the other 41 are untaggable and compose their identity from a stamped parent. The file goes, and the plan is clean. The request count of that plan is kept for step 6.carve a team out- six blocks move to a new root with its own live block, the git half any tool needs. Thenlive-mv -from-estateruns three times in the destination, once for each resource that carries a marker. The inline policy and the two attachments carry none and need no write. The plain CLI reads the role’s new estate tag and its inline policy still attached.both sides plan clean- the monolith no longer declares or owns the six; the team estate declares all six and owns the three. No state was split, nothing was rebuilt.carve across a reference- the ECS execution role leaves for an IAM estate. The task definition that stays reads the same ARN through a data source, the cross-estate pattern the docs give, and all three estates plan clean.a plan costs what its estate holds- the team estate’s plan makes a fraction of the requests the monolith’s did.teardown- the monolith destroys its 71 resources. The IAM estate destroys 2 and the team estate 6. Nothing is left. Every count in this list is asserted by value by the scenario itself rather than typed here from a reading -carve-by-retag.shrefuses unless stock adds exactly 79, unlesslive-importratifies 38 of 79, and unless the three teardowns account for 71, 2 and 6 - so a run is what re-measures them, and those assertions have stood since commite57cc5b4.
The BREAK=1 run does the git half of the carve and never rewrites a
tag. That is the two-ledger window stock lives in, manufactured: the
monolith must name the leavers under owned and undeclared and propose
destroying them, and the new estate must propose creating what it
declares and does not own. Either side planning clean would show the
tag write had changed nothing.
The verb this claim rests on landed with it, and so did the rule it exposed. A move leaves the source’s records for the resource behind, and the source’s next plan found the moved role’s untaggable children in those records and proposed destroying them. The live tag decides. A parent whose marker names another estate never anchors a child for this one, whatever a left-behind record says, and that rule is why step 4 plans clean.
Claim 13: the tag is the boundary#
In stock Terraform and OpenTofu, who owns a resource is a line in a state
file. Changing that line is state surgery: no IAM policy can gate it,
because the cloud never sees it, and nothing in the account records it.
Under choudoufu ownership is a tag on the resource, and a tag write is an
API call the cloud’s own policy engine evaluates per resource. A role can
be fenced to half an estate by a condition on the ownership tag, with the
grant live/MARKERS.md publishes under “Granting an estate”. That fence
binds the credential, not the binary: the same condition governs a plain
AWS CLI call with no choudoufu anywhere in the process, exactly as it
governs choudoufu’s own writes, and what it lets through is not hidden
from the tool either - the next plan reads live tags, not a log of who
wrote them. A carve, one half moving into an estate of its own, is then a
governed write the platform can refuse. The scenario turns the emulator’s
IAM enforcement on for its run; the harness’s own key stays privileged,
and only the two roles the scenario creates and assumes are governed.
The boundary this claim proves is narrow, and it is worth stating exactly
that way. The grant fences three actions by name -
ec2:CreateTags, ec2:DeleteTags and ec2:TerminateInstances - on
resources carrying the ownership tag’s value for the caller’s half. It
says nothing about any other action, and nothing about a resource this
estate does not own. Read it as “this condition governs the actions it
names, on the resources that carry the tag it names,” never as a claim
that IAM fences every write a tool-less actor could make.
Clone https://github.com/INTENTIUS/choudoufu. Confirm Docker is running
(docker info) and the AWS CLI is installed. If Go is not installed,
export CHOUDOUFU_VERSION=<latest tag from
https://github.com/INTENTIUS/choudoufu/releases>. From the repo root run:
just smoke the-tag-is-the-boundary
Explain each step's verdict line to me as it prints. Then run
BREAK=1 just smoke the-tag-is-the-boundary and report both "caught"
lines: the first drops the conditions from Bob's grant, and Bob's write
on Alice's half through choudoufu must go through, which proves the
condition and not the credentials was the boundary; the second repeats
that with no choudoufu in the call at all, a plain AWS CLI write, and it
must go through too.The steps as they print:
the platform stands one estate up, two halves in it- two instances in estate app, one under module.net and one under module.data, with markers stamped by the account.two roles, two halves, one grant shape- Alice may act on module.data.* and create into data; Bob may act on module.net.* and create into net. The evidence line prints the conditions.Alice converges her half- a tag change on the database, applied under Alice’s session.Alice is denied on Bob's half - by AWS, not by this tool- the same kind of change on the gateway. The provider’s CreateTags comes back 403 and the gateway is untouched.Bob converges the same change- his session, his half.Bob, tool-less, is refused on Alice's half - by AWS, with no choudoufu in the call path- under Bob’s session, with nothing of this tool anywhere in the process, a plainaws ec2 create-tagsand a plainaws ec2 terminate-instancesagainst the database both come back refused. The same condition that governs choudoufu’s own writes governs a script’s.Bob's own half, tool-less, and the platform lets it through - the next plan sees it- the identical plain CLI call against the gateway, Bob’s own half, lands with no choudoufu involved, and the nextchoudoufu plannames the drift and proposes reconciling it - nothing the fence permits is invisible to the tool. Bob then reconciles it with an ordinary apply.the carve begins with a git move, and Bob's attempt at the retag is denied- the data module moves to a new root, and Bob’slive-mv -from-estate=appis refused by the platform before anything moves.Alice completes the carve: one governed tag write- the same command under Alice’s session, and tofu-estate becomes data.both estates plan clean, each under its own role- No changes in data under Alice and in app under Bob.teardown - each estate by its own destroy.
The BREAK=1 run replaces Bob’s grant with the same reach and no
conditions, then has Bob change a tag on Alice’s half. The write must go
through. If the platform still refused, something other than the
condition was the boundary and the claim would prove nothing. It then
repeats the write with no choudoufu at all - a plain aws ec2 create-tags
under Bob’s session - and that must go through too, or step 6’s refusal
above would have measured a check this tool runs before calling the API
rather than the condition itself.
One emulator note. Real EC2 refuses with UnauthorizedOperation; the
emulator refuses with a 403 whose body the EC2 SDK cannot parse, so the
provider prints api error UnknownError. The scenario matches both, and
the gap is filed as lex00/floci#189.
On the real account, the same carve ran in us-east-2 on 2026-09-03 with the roles assumed through STS. Every governed write was in the account’s own CloudTrail event history within a minute. The two refusals carry the code real EC2 uses, and each record names the session that was refused:
04:39:31Z alice OK Name=database-v2 i-01e1006285c2b37b3
04:39:47Z alice Client.UnauthorizedOperation Name=gateway-v2 i-0d3d2031d0b946a23
04:40:02Z bob OK Name=gateway-v2 i-0d3d2031d0b946a23
04:40:32Z bob Client.UnauthorizedOperation tofu-estate=boundary-data i-01e1006285c2b37b3
04:40:40Z alice OK tofu-estate=boundary-data i-01e1006285c2b37b3Each line is one ec2:CreateTags event, and
live/smoke/evidence/the-tag-is-the-boundary.cloudtrail.json holds the
five with their event IDs and the lookup that returned them. No state
file could have produced that record, because a state edit is not an API
call. The estate was torn down afterwards and the account listed back to
baseline.
Claim 14: a plan costs its estate, not its account#
A bound state file makes a terralith’s plan pay for the whole account: every resource anyone owns sits in the one file every plan reads end to end. Here ownership is a tag, not a file, so a plan of one estate reads only that estate’s resources - and stays that cheap no matter how large the rest of the account grows around it.
Clone https://github.com/INTENTIUS/choudoufu. Confirm Docker is running
(docker info) and the AWS CLI is installed. From the repo root run:
just smoke plan-cost-tracks-the-estate
Explain each step's verdict line to me as it prints. Then run
BREAK=1 just smoke plan-cost-tracks-the-estate and report the "caught"
line: it replans the same estate account-wide instead of scoped to its
own tag, and the cost must jump to the account-wide shape.The steps as they print:
stand up one estate, and plan it alone- a four-resource network estate (a VPC, two subnets, a security group) applies, then plans. Its request count is recorded.grow the account with another estate, and replan the first- an eight-resource estate joins the account under a different tag. The first estate replans to the same request count as step 1, whether or not the second estate exists.what reading the whole terralith would cost- an account-wide, adoption-only scan of the same account costs measurably more than the estate-scoped plan - the shape a bound state file would force on every plan, regardless of which estate you actually meant to touch.teardown- both estates destroyed.
The BREAK=1 run makes the same request the account-wide scan in step 3
made, against the same estate step 1 and 2 scoped for free. If the cost
did not climb to that account-wide shape - more than triple what scoping
cost, the threshold the scenario checks - something other than the
estate scoping was keeping the plan cheap, and the claim would prove
nothing.
Claim 15: apply exactly what was approved#
CI runs Terraform as: plan on the pull request, a human approves, apply
exactly what was approved. The artifact that crosses that gate is the
plan file, and here it stays the stock one - plan -out=FILE, apply FILE. What changes is what the apply does with it. It never replays the
file. It reads the live system and plans against what is there now, the
way every live-markers run does, and then compares its own fresh plan
with the one the file describes: same resources, same actions, same live
objects, and the same values planned for them. Matching, it applies
without asking again, because the file was the approval. Differing, it
refuses by name and exits 3, which is a pipeline’s signal to send the
change back to review rather than to page somebody about a broken run.
Values are compared canonically, not byte for byte: map and object keys
sorted, sets compared by their elements rather than their order, every
scalar carrying its type so the string "3" is not the number 3. Two
things are deliberately outside the comparison. An attribute that is
unknown at plan time - “known after apply” - on either side is skipped,
so a value the provider only settles during the apply can never make a
matched artifact refuse. And a sensitive value is compared as a stable
sha256 digest of its canonical rendering: a moved secret still
refuses, and no secret is ever printed.
Clone https://github.com/INTENTIUS/choudoufu. Confirm Docker is running
(docker info) and the AWS CLI is installed. From the repo root run:
just smoke apply-what-was-approved
Explain each step's verdict line to me as it prints. Then run
BREAK=1 just smoke apply-what-was-approved and report the "caught" line:
it leaves the world unmoved, and the same file must APPLY - a comparison
that refuses every plan file it is handed would prove nothing.The steps as they print:
stand the estate up- the fixture applies, every resource carrying its ownership markers.the change under review- a log group’s retention goes from one day to three, andplan -out=approved.tfplanwrites the stock-format file a pipeline would attach to the pull request.the world moves while the approval waits- a subnet appears in the account carrying this estate’s markers for an address the configuration does not declare, so the next plan proposes destroying it: a change nobody approved.apply the approved plan- the apply re-reads the live system, compares, and refuses. The scenario asserts the refusal’s own summary line, that the row it prints isaws_subnet.crashed Delete subnet-..., and that the exit status is 3.the same change, a different value- the subtler failure, and the one a comparison over resource names alone would wave through. The out-of-band subnet is removed so the change sets agree exactly, and the configuration is edited after the approval: fourteen days of retention instead of the three that were reviewed. Same resource, same action, same live log group, different planned value. The scenario requires exit 3 again, the refusal saying the two plansdisagree about the values it writes, and the attribute named -after.retention_in_days.re-plan, re-approve, apply- the way forward the refusal names. The same two commands over the world as it now is, and the approved change lands: the log group’s retention reads 3.teardown- the estate destroyed.
The BREAK=1 run is the inverse control, and it is the one this claim
needs. A refusal that fires for every plan file handed to it is not a
check, and it would pass step 4 forever. So BREAK=1 skips the
out-of-band change and the same file must apply cleanly; the scenario
fails if it refuses.
Claim 16: the boundary holds across provider configurations#
A state file has no notion of a region: it is one ledger, and two regions
in it are two sets of rows that nothing keeps apart except the addresses
you chose. Here the boundary is real. An estate spans regions and
accounts under one tofu-estate marker and one record store, and every
answer a plan gives is about exactly one provider configuration - which
region it listed, which region it read, which region it is proposing to
change.
The case that makes this concrete is the one people actually write:
mirroring a client-chosen name into two regions. A log group named
/app/logs in us-east-1 and a log group named /app/logs in
us-west-2 are two distinct objects that answer to one import identity,
and evidence about one of them says nothing about the other. Choudoufu
got this wrong once - the cache’s existence vouches were keyed by
identity alone, so the surviving region’s object could vouch for a
deleted instance in the other one, and the plan reported a dead resource
as unchanged (issue #745, fixed in #837). Step 2 is that failure, made
into a claim you can run.
Clone https://github.com/INTENTIUS/choudoufu. Confirm Docker is running
(docker info) and the AWS CLI is installed. If Go is not installed,
export CHOUDOUFU_VERSION=<latest tag from
https://github.com/INTENTIUS/choudoufu/releases>. From the repo root run:
just smoke the-boundary-holds-across-regions
Explain each step's verdict line to me as it prints. Then run
BREAK=1 just smoke the-boundary-holds-across-regions and report the
"caught" line: it points the west provider at the east region so the
same name in the other region is the only live evidence for the deleted
instance, and the run must fail to name it.The steps as they print:
two regions, one estate- two aliased provider configurations,aws.eastandaws.west, one region each. Both hold a log group with the same name; each holds a VPC of its own; a single S3 bucket is declared only underaws.east. The AWS CLI reads both regions directly and shows two distinct region-qualified ARNs for one name, both carrying the same estate marker and each its own address marker, with one record store beside the module holding both. Then a-refresh=falseplan is empty, and the work is attributed per provider configuration by the region each request was signed for - SigV4’s credential scope, read off the wire rather than from a counter of ours. The same stream shows what each pass listed: the mirrored type once per region, because a region-scoped list is the only way to see both objects; the east-only bucket once across both passes, becauseaws.westdeclares none of it and S3’s list is account-global.a delete in one region is seen in that region-aws.west’s log group is deleted with the AWS CLI.aws.east’s identical name is untouched. The next-refresh=falseplan must nameaws_cloudwatch_log_group.westand nothing else, andaws.east’s own instances must still be served from the cache. One ordinary apply puts the missing half back - one create inus-west-2, nothing inus-east-1.an orphan in a region nothing declares any more- the answer to “does the sweep still look there”, pinned rather than argued, because it is the unflattering one. The unit of the sweep is the provider configuration, not the region. Drop one block whileaws.westis still configured for something else and the sweep still listsus-west-2and names the orphan there. Dropaws.west’s last declaration and the provider configuration goes with it: nothing points at that region any more, the sweep stops looking, and the marked objects sit inus-west-2with no run proposing to remove them. They are not lost - their markers still say whose they are, and putting a provider configuration for that region back brings them straight back into the sweep, which is what step 4 does before it destroys them. But no plan will mention them while nothing points at the region.recovery is a re-run in both regions at once- claim 5 with two provider configurations. The state cache and the whole record store are deleted and the same plan runs again. The change set must be identical, because both regions come back from what the cloud itself carries - markers for the server-assigned instances, the configuration’s own names for the client-named ones. The coverage report must not be identical, and the scenario checks that too: with no record to read, each region has to be listed for markers, and a run reporting the same coverage would be a run that never noticed the files were gone.a region change is a replace: refused by default, permitted by name- one VPC’sprovidermoves fromaws.westtoaws.east. That is a replace, not a move: no cloud API relocates a VPC between regions, andlive-mvrewrites ownership tags rather than resources. Only half of the replace is expressible, because a resource address carries exactly one provider configuration in the plan graph - taken from its own block- so the destroy of the object left behind in
us-west-2cannot be planned at that address at all. The step runs both of the two answers the schema offers for the half that cannot be planned. By default the run refuses: the plan names the live VPC, the region it is in, the region its address now points at, and the four things an operator can do about it, one of which is the toggle. Proceeding would leave two live resources carrying this estate’s marker for one address, which is what live/MARKERS.md’s ownership semantics forbid and whatcrossProviderOrphanCollisionsalready refuses a plan over once both objects exist. Withstrict { provider_change = "recreate" }the plan proceeds - stock OpenTofu’s own outcome - and the same finding comes back as a warning naming the object, where it is, and the fact that no plan will ever propose anything for it. The toggle buys the create, not the silence, and in neither mode does anything claim marker discovery will find the old object: issue #906 and its maintainer ruling of 2026-09-06.
- so the destroy of the object left behind in
teardown- one destroy removes exactly what the two provider configurations hold between them.
This scenario runs the region axis. The account axis is claim 19, a sibling scenario with the same estate and the same steps, differing by account instead of by region - the reasoning that the two are one mechanism (the provider configuration is the partition key in both cases, and nothing in the sweep, the vouch partition or the orphan classifier reads a region as such) is now measured rather than asserted.
The BREAK=1 run inverts step 2’s world rather than its assertion. The
check that has to be load-bearing is that the dead instance is named,
and the defect’s symptom was silence, so the control has to make silence
happen: it strips the ownership markers from the surviving object (an
unmarked sighting is the only shape the cache’s envelope-vouch arm
consumes) and points aws.west at us-east-1, so the west pass lists
the region the surviving object lives in and sights the same name. The
cache then serves the deleted instance and the plan reports it
unchanged - and the scenario fails on exactly that line.
Claim 17: a record-only composite identity survives cache loss without a duplicate create#
This claim covers one specific, narrow class of resource: untaggable, unlistable, composite-identity types, where the record store this fork writes on every apply is the only place an instance’s identity is ever held - no tag to carry it, no listing that could rediscover it. For that class, “the record is a cache” is not true the way it is for a tag-governable resource: lose the record and there is nothing left to recover from, so the honest answer is a named duplicate create, never a silent bind and never a silent “no changes.”
aws_iam_group_policy is this fork’s worked example. It carries no
tags argument, and left to the ordinary default - the name argument
absent, the provider assigning one - its two-part identity (the IAM
group, the assigned policy name) has nowhere else to come from: the
policy name is not a literal in configuration and not a reference to any
other resource’s own argument, because the provider invents it at create
time. Once the object exists, the record this apply writes is the only
copy of that pairing.
This is a different resource than the one issue #746 and PR #851
measured. That PR’s own re-measurement, against a real hashicorp/aws
6.59.0 provider schema, named 27 admitted types that are markerless,
unlistable and carry a wire-identity composite (a provider-native
identity_schema, several attributes, no documented separator) - and of
those, only three actually reach the located-fallback bind PR #851 fixed
(aws_datazone_glossary_term, aws_opensearchserverless_security_config,
aws_redshift_namespace_registration); the other 24 are already diverted
to the record-located class earlier, through a different door. None of
the three is implemented by the pinned floci image (probed directly:
datazone, opensearchserverless and redshift-serverless are absent
from its service list, and redshift register-namespace itself answers
UnknownOperationException). The other 24 all carry a ratified,
hand-written identity-table row whose components are literals or
references this fork’s own static evaluator can fold from configuration
alone - measured directly against this same pinned image, every one of
them survives losing its record exactly because configuration alone
rebuilds the same identity, record or no record. Neither population,
today, can demonstrate this claim on the emulator.
aws_iam_group_policy reaches the identical recovery mechanism
(identity.ClassRecordLocated, projection.materializeLocated) through
its own ratified table row, whose policy-name component is marked
server-assigned-if-absent rather than through a wire identity_schema -
an older, independently-arrived-at admission for the same class of
problem. It is markerless and unlistable in exactly the sense PR #851’s
population is, and IAM is a service the pinned floci image fully
implements, so it is the nearest floci-servable member of this claim’s
real population: untaggable, unlistable, composite-identity resources
whose identity has at least one component the provider - not this run’s
own configuration - assigns.
Clone https://github.com/INTENTIUS/choudoufu. Confirm Docker is running
(docker info) and the AWS CLI is installed. From the repo root run:
just smoke record-only-survives-cache-loss
Explain each step's verdict line to me as it prints. Then run
BREAK=1 just smoke record-only-survives-cache-loss and report the
"caught" line: with the identity record deleted, the plan must propose
one create, named, instead of quietly reporting no changes.The steps as they print:
stand the estate up, and read the record- an IAM group and one inline policy on it apply, the policy’snameleft for the provider to assign. The record this apply writes is read back and its group and policy name are printed by value - no tag, no listing, holds either one.lose the disposable cache- the cache and the whole.terraformdirectory are deleted, the same disaster claim 5 recovers from. The re-plan reportsNo changes., and the scenario checks the debug log’s ownGetGroupPolicyread: the group and policy name it actually used are compared, by value, against the ones the record printed in step 1 - not a passing count alone.lose the record too- the honest answer is a duplicate, by name. WithBREAK=1, the identity record itself is deleted before this same re-plan (cache and.terraformgone as well): nothing anywhere can say which live object the policy owns, and the plan must propose exactly one create, namingaws_iam_group_policy.app. WithoutBREAK=1, the same cache-loss recovery runs a second time with the record intact, for contrast: stillNo changes.teardown- the group and its policy destroyed (or, underBREAK=1, cleaned up by hand, since the proposed duplicate was never applied).
Claim 18: a replaced object’s shadow is not a second claimant#
When an apply replaces a resource, the destroyed object does not stop
answering straight away. A terminated EC2 instance still comes back from
describe-instances, still lists in the tagging API, and still wears the
two ownership tags it was stamped with. The next plan therefore finds two
objects claiming one address, and tags alone cannot tell a corpse from a
rival.
So the apply that destroyed the object writes it down. A tombstone is one entry in the address’s own record naming an identity this estate’s own apply destroyed - a list of them, capped at eight per address, oldest evicted first - and it is evidence that an object is dead, never permission to touch one that is not: the only thing an entry can do is drop a claimant out of a collision set, so an entry that is wrong costs a refusal and can reach the live system through nothing.
Clone https://github.com/INTENTIUS/choudoufu. Confirm Docker is running
(docker info) and the AWS CLI is installed. If Go is not installed,
export CHOUDOUFU_VERSION=<latest tag from
https://github.com/INTENTIUS/choudoufu/releases>. From the repo root run:
just smoke a-shadow-is-not-a-claimant
Explain each step's verdict line to me as it prints. Then run
BREAK=1 just smoke a-shadow-is-not-a-claimant and report both "caught"
lines: it puts a second genuinely running instance behind the same address
marker, and the plan must refuse rather than prune it; then it patches the
record to call a running, deposed object destroyed, and the read must
refuse that.The steps as they print:
stand the estate up- one instance, one record naming it by its server-assigned id.force a replace at the same declared address-subnet_idis ForceNew, so moving the instance to the other subnet destroys one object and creates another at the same address.the destroyed object's tags are still readable- the plain AWS CLI, with no choudoufu in the loop, reports the old instance asterminatedand still tagged for this estate and this address.the record says which one it destroyed- the record file is read off disk:identity.import_idis the live object, andtombstoneis a list. A second replace runs and the list grows to both destroyed ids, which is what makes the cap of eight a cap on a list rather than a flag.the shadow arm- the plan runs with both shadows still listed. It exits 0, drops exactly the two identities the record names as destroyed, names each of them in aLive resource displaced from the address it is marked forwarning that proposes nothing, and binds the address to the third.the honest boundary- what a tombstone authorises, which is one claimant leaving a collision set and nothing else.a failed destroy leg writes no tombstone- the other half of the write side. A role that may do everything exceptec2:TerminateInstancesis created and its fence confirmed with the plain CLI first. Under that role, withcreate_before_destroyon, a third replace creates the new instance and the platform refuses to terminate the old one, so the apply exits non-zero. The CLI reports the old instancerunning; the record file names the new one asidentity.import_id, the old one underdeposed, and the old one nowhere undertombstone, while step 4’s two entries are still there. The next plan carries the deposed object to its destroy rather than pruning it. Nothing destroyed it, so nothing says it was.teardown- the estate destroyed, the deposed object with it.
The BREAK=1 run has two arms. At step 5 it creates a second, genuinely
running instance carrying the same estate and address markers as the
survivor, with nothing recorded as having destroyed it. The plan must exit
non-zero with Two live resources claiming one address, naming both live
ids. This is the arm that makes the claim load-bearing: the same shape used
to be waved through with a warning and exit 0, and a mechanism that quiets
a dead object’s marker is only safe if it still refuses a live one. At step
7 it patches the record by hand to list the running, deposed instance under
tombstone, the entry the write side produced before #901, and the read
must catch it: an assertion that only ever reads an empty list is not
load-bearing.
Claim 19: the boundary holds across accounts#
Claim 16 proves the boundary across two regions. This is the same
estate with the other axis swapped: two AWS accounts, one region,
one tofu-estate marker and one record store. It is a separate claim
because “a cross-account alias is the same mechanism as a cross-region
one” was, until this ran, a piece of reasoning - the provider
configuration is the partition key in both cases, and nothing in the
sweep, the vouch partition or the orphan classifier reads a region as
such - and reasoning is not a measurement.
The case is claim 16’s, sharpened. There, two objects with one
client-chosen name were kept apart by their regions. Here they are in the
same region, and the only thing that tells them apart is which
account they are in. A log group named /smoke-two-accounts/app in
account 000000000000 and one named /smoke-two-accounts/app in account
111111111111 are two distinct objects answering to one import
identity, and evidence about one of them says nothing about the other.
The emulator says so itself: applying both blocks against a single
account fails the second create with
ResourceAlreadyExistsException: The specified log group already exists,
and against two accounts both succeed.
Second credentials are all it takes to write this configuration. The
account id is the access key id - the pinned emulator reads a 12-digit
access key id as the account itself, and resolves an sts:AssumeRole
session into another account’s role the same way - so the two provider
blocks in the fixture differ by exactly one argument, and SigV4’s
credential scope then carries the account id on the wire the way it
carries the region.
Clone https://github.com/INTENTIUS/choudoufu. Confirm Docker is running
(docker info) and the AWS CLI is installed. If Go is not installed,
export CHOUDOUFU_VERSION=<latest tag from
https://github.com/INTENTIUS/choudoufu/releases>. From the repo root run:
just smoke the-boundary-holds-across-accounts
Explain each step's verdict line to me as it prints. Then run
BREAK=1 just smoke the-boundary-holds-across-accounts and report the
"caught" line: it swaps the second provider's credential for the first
account's, so the same name in the other account is the only live
evidence for the deleted instance, and the run must fail to name it.The steps as they print:
two accounts, one estate-sts:GetCallerIdentityunder each credential answers with a different account id, with nothing of this fork in the loop. Then one apply over both provider configurations: two log groups with the same name in the same region and two different account-qualified ARNs, each carrying the sametofu-estateand its owntofu-address, and each invisible to the other account’s own listing - which is what makes them a pair rather than one object seen twice. Each account also holds a VPC of its own, server-assigned, and one record store beside the module holds both accounts’ instances. Then a-refresh=falseplan is empty and the work is attributed per account by the credential each request was signed with, read off the wire. The estate-wide tag index is counted the same way, off this fork’s own client’s request line: one fetch signed as each account and none unsigned, so the second account’s sweep is measured rather than assumed (#957).a delete in one account is seen in that account- the log group in account111111111111is deleted with the AWS CLI. The identical name in account000000000000- same name, same region, same service - is untouched. The next-refresh=falseplan must nameaws_cloudwatch_log_group.other_accountand nothing else, and the home account’s own instances - the needs-discovery VPC and the client-named log group alike - must still be served from the cache. One ordinary apply puts the missing half back: one create in111111111111, nothing in000000000000, and the recreated object carries the other account’s ARN.recovery is a re-run in both accounts at once- claim 5 across two accounts. The state cache and the whole record store are deleted and the same plan runs again. It must be empty, because both accounts come back from what the cloud itself carries: markers for the server-assigned VPCs, the configuration’s own names for the client-named log groups. The run is also required to have signed requests as both accounts, so an empty plan cannot be an empty plan that never looked. Had either account’s half been unreachable with the record gone, this step would have proposed creating a resource that already exists.teardown- one destroy removes exactly what the two provider configurations hold between them, confirmed by reading each account directly afterwards.
The BREAK=1 run inverts step 2’s world rather than its assertion, the
same way claim 16’s does. The check that has to be load-bearing is that
the dead instance is named, so the control has to manufacture silence:
it strips the ownership markers from the surviving object in
000000000000 (an unmarked sighting is the only shape the cache’s
envelope-vouch arm consumes) and swaps aws.other_account’s credential
for the home account’s, so the second pass lists the account the
surviving object lives in and sights the same client-chosen name. The
cache then serves the deleted instance - state cache hit for aws_cloudwatch_log_group.other_account, listed live this run, ownership record-attested - the plan reports it unchanged, and the scenario fails
on exactly that line.
Claim 20: scale, cited rather than re-run#
Every claim above runs a scenario against the pinned emulator, at a scale a reader can stand up in a couple of minutes. None of them speaks to whether the design holds at the scale a real estate actually reaches, because that is not something a laptop and a Docker container can measure honestly. It has been measured, on real AWS, and published on what you pay, and when and what a plan costs - just never carried onto this page. This claim carries it here, cited rather than re-measured for the purpose, and draws the line around exactly what it does and does not say.
A 745-resource estate migrates in one pass, with a single state file
behind it. Real AWS, us-east-2, recorded in
live/gauntlet.json’s
live_cert block at commit 1d06e1d177: stock terraform applied 745
resources holding its own state file; choudoufu live-import -approve
verified 335 of the 745 and stamped every one it verified, and left the
other 410 alone because they compose their identity from an already-stamped
parent and need no marker of their own - nobody typed one by hand. The
post-migration plan came back empty and the no-op apply changed nothing.
One estate, one state file, one migration pass, at a scale most terraliths
never reach.
Provider call counts hold at parity with stock, or under it, at that scale. The same real account, both sides planning a no-change estate - what you pay, and when’s “same comparison on real AWS” table:
| Resources | stock | choudoufu | Difference | Commit |
|---|---|---|---|---|
| 79 | 149 | 155 | +6 (+4.0%) | d359210978 |
| 745, session 1 | 1416 | 1413 | -3 (-0.2%) | d359210978 |
| 745, session 2 | 1449 | 1404 | -45 (-3.1%) | 02885d2fd6 |
At 79 resources choudoufu costs six more requests than stock. At 745, in two separate real-AWS sessions, it costs fewer. The comparison does not worsen as the estate grows; at this one scale it inverts.
The sweep that makes migration and recovery possible is shaped by the
provider’s admission table, not by the size of the account it runs
against. What a plan costs’s
own reproduction, no cloud and no emulator, commit 5d55f4aa9f:
go test ./internal/live/discovery/ -run TestSweepUniversePartitionIsMostlyNative
sweep universe=1027 tagging_leg=35 native_leg=992That bounds the sweep’s shape - one call per admitted type, not one call per
object the account holds. Whether the account’s own object count could
still leak in through the one per-object refinement call the native leg
makes was a real, named risk
(#622), until it was
measured directly: on a real, populated account of its own - 24 IAM roles,
5 buckets, 2 hosted zones, 11 active ECS task definitions, none of them
this estate’s - at commit eb1d145dc5, that call fired zero times, at
a small scale and at ten times it, on the first plan and the steady-state
one alike. For a terralith shaped like the ones this repository generates,
the sweep is O(admitted types) - not O(account objects), the shape a state
file forces onto every adoption everywhere else.
What this claim does not say. It says nothing about incremental plan time within one already-adopted state; the day-2 call counts on what you pay, and when and what a plan costs are their own, separately measured figures, and this claim does not restate them as if they were part of it. And the seconds comparison - how long a plan takes on the wall clock, never how many requests it issues - stays exactly where what you pay, and when leaves it: withdrawn, because the sessions that produced one compared a cached plan against an uncached one. This claim will not restate a number its own source page has already taken back; re-measure it there; this page will follow once that page does.
This is the one claim on this page with no smoke scenario and no BREAK=1
control, and it says so rather than reading like the other nineteen. There
is nothing here to run: the evidence is the cited pages and the cited
commits, and a reader who wants to challenge this claim should challenge
those - site/content/docs/what-you-pay.md,
site/content/docs/model/plan-cost.md, and issue #622 - rather than look
for a scenario that does not exist.
Reading a run#
Every scenario narrates each step the same way: first why the step
exists and the exact command it runs, then real output indented as
evidence under a verdict line starting with ->. The final paragraph of
each run recaps what you watched. The
harness page
documents every knob; pinning the emulator image and the choudoufu
version are both there.