The release where three runtime paths became one contract, and the whole of
feint was replayed to check the simplification was not paid for in fidelity.
Two packs of three had quietly forgotten to hand their security groups to the
host, and it survived for months because every test was green (#475). So the
refactor was the means and the proof was the point: an acceptance gate that adds
no product code and cannot close until the API, Terraform, the real clients, real
machines, networks, firewalls and balancers have all been driven again (#545).
They were. 220 verdicts under incus-ovn with one skip — named, pointing at
where its proof lives instead — 988 falsification mutations of which none
survived, and no axis of evidence fell against 0.11.0.
That last sentence is the one this release had no way to check. The invariant was
written in the milestone and verified by hand, so the machinery for it was
brought forward from 0.15.0: a downstream project can now pin the level of proof
it depends on and have its own CI fail when feint stops delivering it (#488).
The instrument was built rather than the claim asserted.
Added
-
A downstream project can pin the level of proof it depends on, and its own
CI fails when feint stops delivering it:feint evidence baselineand
feint evidence verify, CLI surface 21 (#488). Not "feint changed
version" — "feint stopped proving what this project was relying on".
#325 and #326 exist because a consumer discovered a change from the outside,
after the fact.This is not
drift:check, and the boundary is the design: that one
watches the upstream SDK surface — has the cloud moved? — and this one
watches this emulator's own level of evidence, from the point of view of
somebody who is not in this repository — has what I trusted moved? A design
folding one into the other answers one question twice and the other never.Only what is proven is pinned. An axis whose verdict is the absence of
proof —false,none,unchecked,unobserved— is dropped at capture,
because pinning it turns every later improvement into a regression. Measured:
the first version of this pinned them, and run againstv0.11.0it reported
osc/Client.AcceptNetPeering behaviour: false → trueas a fall.Claims are meant to be withdrawn here. #475, #481 and #483 are each "this
was claimed and should not have been", and a baseline that only grows would
have stopped all three.--acceptedcarries a withdrawal with its reason,
on the modelcorpus/accepted.jsonalready uses; an entry with no reason is
refused rather than honoured, because the whole value of the file is that a
withdrawal carries why.A baseline from a partial run is worse than none, so a record earned with
no machine runtime is refused at capture: it pinsdataplane: falseon every
operation that would have earned it, and the consumer is then told nothing
regressed on the day it did.mise run evidence:verifyholds the tree against the previous release. Run
today againstv0.11.0: 364 operations pinned on seven axes, every level
still delivered, which is 0.12.0's release invariant measured by an
instrument rather than by hand. -
The operator declares which projects the emulated account holds:
cloud.projectsinfeint.yaml,serve --projects, and the CLI surface
moves to 20 (#572). Until now this emulator held one project, named
default, and a stack whoseproject_namewas its own production project
died on the Terraform provider'sFindExactafter a truthful empty list —
the obstacle for exactly the person #372 exists to serve, a platform team
pointing an existing stack at the emulator.data "scaleway_account_project"
now resolves a declared name, andGetProjectreads it back as itself
instead of answeringdefaultfor everything.The list still filters. A name nobody declared answers an empty list, and
that is the whole design: the cheap fix was to answer that a project exists
because somebody asked for it, which is the class #83 measured and closed on
all three packs — an identifier no catalogue held, silently substituted, and
a green run that meant nothing. The inventory stays something the operator
stated.Identifiers are derived from the name and never minted per run, for the
reason the project'screated_atis fixed: a value that moved between two
reads is a permanent Terraform diff.defaultkeeps the identifier every
other product of this pack already scopes its answers to, so a declaration
that omits the field — every declaration written before today — gets exactly
the account it had. -
A word for the shape the vocabulary could not describe:
capabilities.firewall_public_when_joined, and/_feint/healthmoves to
schema 7 (#548). A consumer holdingcapabilities.firewall: true,
enforced.firewallandfirewall_public_only: falseconcluded that the one
uncovered case was the machine with no private network. It was not, and the
fix below is why the field exists: a machine that does join an emulated
network now ends up with its published address on the interface that wears
its rule sets, and that is a different claim from the one version 5 added.
Both are true at once, and both are needed: the older field is stillfalse,
because a machine whose only interface is routed has nowhere to move an
address to. The new one is a claim about the runtime and not about every
pack — a pack that names no emulated network for its public addresses keeps
them on a routed NIC, which Exoscale does on purpose. -
The example stacks are applied to real machines every night, and the gate
that does it says how many times (#504).conformance:functionalis the
only thing here that appliesexamples/stacks/against a runtime, and
nothing in CI played it: no leg ofruntime-proof.ymlapplies a stack, and
mise run conformanceskips the stack suites without one. It is what
surfaced the isolation-detach race that cost three pull requests (#577,
#578), on a defect older than the whole range bisected. It now runs as a job
of its own on that workflow — a job rather than a step, because the incus-ovn
leg's ssh suites were red on four of the last seven scheduled nights and each
of them leaves rule sets and networksfeint stopdoes not sweep, so a step
there would queue behind a red and then meet a doorstep it could not pass.
Three passes, and the number is an arbitrage: the defect class struck 9
times in 13 runs, so one pass calls it absent 31% of the time, and three bring
that to 3%, at 295 s a pass. -
The Account product's projects, so a third-party VPC stack reaches a VPC
path at all:account/v3/ProjectAPI.ListProjectsand
account/v3/ProjectAPI.GetProject(#372).data "scaleway_account_project"
is evaluated ahead of every resource in a Terraform graph, and it answered
501: two published modules that walk the VPC surface died in two exchanges
each, before a single VPC, ACL, private network, gateway or IPAM address was
attempted. Both routes are mounted because the provider's own read calls both
— ListProjects when the configuration names the project, then always
GetProject on the id it resolved. The wholeaccountproduct joined the drift
gate with them: twelve operations, two served, ten declined with a reason.The list answers one project, named
default, and filtersnameand
project_idshonestly — a name this emulator does not carry answers an empty
list. The read echoes any identifier, which is
limits.md's "identifiers are not checked against anything"
applied to the value a stack most often carries over from production. The cost
is stated there too: a stack whoseproject_nameis notdefaultfails on
the provider's ownFindExact. -
A capability matrix owns every sentence that claims a client for a
provider, anddocs:checkreads the pages back (#592).README.md:41said
"Run your Terraform against Scaleway, Outscale or Exoscale" while
docs/confidence.mdsaid the opposite andfeint uphad been refusing
iac.engine: terraformfor that pack since #525 landed on 2026-08-26. Every
doc gate stayed green throughout, and none of them was wrong: they compare
values an artefact also holds, and a claim about capability is not one.
The matrix (internal/cli/capability.go) is provider × client × mode ×
support × proof × reason, and the proof column is what makes it more than
a table: asupportedrow is resolved against the conformance workflow that
drives that pair, arefusedrow against the pack's ownVetoEngine— the
codeupanddownconsult — and both directions are checked, so a veto
nobody wrote down and a row nothing proves are equally refused. The README's
promise is generated from it in both languages, and every generated block of
the front pages is read back: a sentence naming a refused pair passes only if
it also names the upstream issue that would change it. Exit code 2, like the
rest of the chain. -
The Quick Start is four commands a reader can copy, on a stack short enough
to read whole (#593). It taught the 0.10 sequence —feint start,eval "$(feint env scaleway)",terraform apply— with no directory, nomain.tf
and no provider block, under anApply complete! Resources: 5 addedthat was
not reachable from it.examples/quickstart/scalewayand
examples/quickstart/outscaleare the first example now: a provider, an
address, one machine, 34 and 43 lines of Terraform,runtime: mode: off.
examples/stacks/is unchanged and keeps its job — it is the qualification
stack that found #249 and #250, and it was being asked to be a first read as
well. The apply line is derived from the configuration and
tools/conformance/quickstart.shlifts it out of the README and requires the
run to print it, so the output shown is the output produced. The suite runs
feint up, an empty second plan andfeint downon both examples, on the
terraform and opentofu legs, and the quickstart directory joins the population
feint docs --checkalready judges: applied by CI or declared with a reason,
and pinning the provider that answered. -
The generated blocks that carry prose are rendered per locale (#591). The
French README opened with an English Quick Start — "On your machine", "In CI,
or anywhere Docker runs" — because the generator injected one block into both
pages on the rule that a command needs no translation. True of the commands,
and they are still shared along with the version, the image and the
repository; the sentences around them are written twice.
Fixed
-
A single
feint docsrun that changed two sections of a README kept only
one of them. The target was written from a copy spliced at the top of the
run, after the helpers that re-read the same file and splice into what they
find — so it put their sections back and reported success, and the only
symptom wasdocs --checkstill red after a regeneration that had said
README.md updated. Measured while adding the promise block. -
A server created with its public IP no longer keeps an unfiltered
interface beside its filtered one (#548). Created with anip_id, a
Scaleway server boots carrying only that address, so the driver gives it a
routed NIC (#202) — an interface Incus accepts no security option on at all
(#337). The private NIC arrived afterwards, took the rule set, and left the
published address on the bare one: measured 2026-08-27 on
examples/stacks/scalewayand reproduced from the API alone on 2026-08-28 in
both driver modes, a port the group'sdropdefault never opened
answered from the station, with a listener proved inside the machine and a
bare port as the negative control.The address moves now.
RouteAddressreleases it from the routed device
without removing the device — the two other remedies were tried and refused,
anddocs/limits.mdkeeps both: the uplink cannot take the/32while the
routed NIC owns the host route, and removing the device unmasks the
profile'seth0on the operator's own bridge — then hands it to the
interface the pack named, which wears the rule sets. After it, in both modes:
443 open because a rule opens it, the port no rule names closed with its
listener still running, and the bare port as the control. The routed device
stays on the instance with no address, and a routed NIC that carries nothing
is no longer reported as an escape. -
What a machine answers on is read, not guessed, and a restarted machine
gets its interface back (#548).Inspectused to answer one address — the
first of the lowest-named interface — and the layer above then decided what
kind of address that was from the pack's declared public block. The two
agreed only while a routed NIC sorted before a managed one, which is exactly
what the move above ends: after it the runtime answers
{"eth0":[],"eth1":["10.199.0.2","203.0.113.2"]}, and the old reading would
have published the private address where it published the public one — for
three packs at once, sinceBindingis the shared layer and the Exoscale
pack reads it. The driver reports every address it saw and settles nothing;
Reconciler.PublicAddressOfandPrivateAddressOfpick out of that set by
the block, which is the pack's own declaration. Nothing a client sees
changes, and an entry a restored snapshot put there that is not an address is
now dropped rather than published — only the public half ever went through a
parser.The restart half was measured the same day and had to be fixed for the move
to survive one: a NIC attached to a running machine is configured inside the
guest by the driver, nothing in the guest remembers that across a reboot, and
the machine came back with no address on that interface at all — the driver's
own ninety-second wait giving up on a lease nobody offers, in both modes.
While the published address rode a routed NIC of its own that cost only the
routes to the peered subnets (#549); once it lives on that interface it costs
the address itself. The start path restores what the device reserves before
it waits.And the record a boot leaves is re-read at the end of the replay, because it
was written before the replay installed the addresses the plan promised: the
same rebooted machine recorded10.199.0.2alone while the station reached
203.0.113.2 on it in the same pass. The pack's own bookkeeping hook moved
with that read, from straight after the start to just after it — it ran early
by exactly those addresses, and the Outscale test that keeps a stopped Vm's
private address is what measured it. -
A graceful exit gives back every piece of host plumbing no client can
delete, and a run that leaks is the run that goes red (#521). The incus-ovn
leg ofruntime-proof.ymlfailed at the doorstep of the next step on a
GitHub runner nothing had ever touched: a previous run left 0 machine(s) and
2 network(s) on this host, namingfeint-uplink,fnt-defaultand the rule
sets of two providers' default security groups. There was no previous run —
the leg's own three ssh suites had made all four, each exiting 0.Four objects, one property: no client call can remove any of them, so
leaving them measured nothing about the suites.fnt-defaultis the network
a machine with no attachment of its own boots on, created here by an Outscale
Vm outside a Net and owned by no resource;feint-uplinkhad been released
since #521 and stayed becausefnt-defaultstill drew from it; thescw-*
andexo-*rule sets belong to default security groups a client cannot
delete, and Scaleway's is minted per run, so one host ACL accumulated per
session. The exit now releases all four, in the only order the runtime
accepts, and each release answers both questions — is it ours, and is
anything still drawing from it — so afeint stopwithout--cleanupleaves
the firewall on the machines it deliberately leaves running.The second half is the sentence.
feint clean --check --closingand
tools/conformance/guard.sh leftovers-afterask the identical question and
name this run,runtime-proof.ymlasks it after its own stop instead of
letting the next step meet the residue, and the doorstep now names the rule
sets it does not refuse on rather than passing over them in silence. The
ledger's attribution column was corrected with them: a network is found by
the labelSurveyreads, not by thefnt-prefix the column claimed — and
the first network the failing leg reported,feint-uplink, carries no such
prefix. -
The stack gate's closing doorstep asks the question its own comment
claimed, and the gate stops choosing its runtime in silence (#504). It
closed withguard_leftovers_for "$RUNTIME" "the end of the run"under a
paragraph saying the doorstep was asked again on the way out; the guard arms
that question on the literaldoorstepalone, so a pass that leaked a
machine or a network exited 0 and the next run met the refusal — the state
#521 removed frommise run conformance, described here and never done. And
it opened withRUNTIME="${FEINT_FUNCTIONAL_RUNTIME:-incus-ovn}", so an
exportedFEINT_VMwas ignored without a word: #574's line one directory
over, on the one gate whose subject is the assertion the two modes disagree
about. The resolution moved totools/runtime-mode.sh, shared with
evidence:update, whose unchanged tests are what prove the move changed
nothing. -
The Exoscale stack's two null outputs are not this emulator's (#520). The
filing deduced, without instrumenting it, that the pack's by-id GET raced its
own async create-completion. Measured on 2026-08-28 against the pack
directly: 100 back-to-back by-id reads of one balancer, 100 fresh
create-then-read pairs, and 50 for the instance pool — zero empty names,
addresses or sizes. There is no window: the create putsnameandipin
the store before it writes an operation that already saysstate: success.
The stack's third by-id door read back correctly in the same apply, so
whatever produced the nulls tells the three apart and this pack does not.
docs/limits.mdcarries the measurement and the disproof; what would lift
the limit is upstreamterraform-provider-exoscale#573, and no change to
this pack would. -
An Exoscale security group stops at the public interface, where the real
cloud stops it (#574). Exoscale states it in one sentence: "Security group
rules do not apply to traffic inside private networks". Sincea344f8d
(#494/#475) this emulator applied them there. The emulateddefaultgroup
carries no ingress rule, so its rule set translates to a drop default, and
the driver wrote it onto the membership NIC: two instances of one private
network could not reach each other. Measured 2026-08-27 under--vm incus,
one network and two instances created with--public-ip none, whose only
interface is the membership one:security.acls=exo-…plus
security.acls.default.ingress.action=dropon the NIC, 0/10 probes
connected; with the fix, nosecurity.aclskey at all, 10/10. Under
--vm incus-ovnthe same wrong rule set was written and did not bite,
because the sender's catch-all egressallowat priority 300 outranks the
receiver's NIC default deny at 100/111 (#491) — so every green that segment
produced under OVN rested on rule ordering, and the after-state is read off
the NIC rather than off a connection succeeding.Where a security group applies is now a provider fact carried by the shared
layer, not a rule of the layer: the pack declares it per interface on
machine.Attachment.Unfiltered,machine.GroupSyncreads it off the plan
the pack already declares, and it reaches the driver on
FirewallBinding.Unfiltered. Scaleway and Outscale declare nothing and keep
filtering their private NICs, which their own network suites assert.
Consequence a reader of the host will meet: an Exoscale instance with no
public address and only private memberships carries no rule set on any
interface, because it has no interface a group covers. -
The three instruments that hid it, repaired in the same change (#574).
.github/workflows/runtime-proof.ymlran two of the three network suites and
nottools/conformance/exoscale/network.sh, on either leg, so nothing in CI
replayed it;TestEveryNetworkSuiteIsReplayedByTheRuntimeProofnow derives
the list from the suites on disk rather than from a list somebody remembers.
mise run evidence:updateoverrode an exportedFEINT_VMin silence on its
runtime leg (FEINT_VM="${FEINT_EVIDENCE_VM:-incus}"), which manufactured
two successive false attributions during the diagnosis;tools/evidence/mode.sh
now honours the caller, refuses a disagreement and--vm offby name, and
announces the mode it runs before anything starts. And the Exoscale
network suite's own header claimed the pack "does not yet sync its security
groups onto the machines" — true beforea344f8d, read as a live fact for
months after, and what steered every reader away from the firewall. -
A server answer carries
bootscriptandextra_networks(#366), a
catalogue image typesfrom_serveras a string (#367), and an attached public
address publishes its gateway and its own tags (#368). Four response shapes
a client can observe, all four measured against a real fr-par account on
2026-08-21 and again on 2026-08-24 and all four recorded in
corpus/accepted.jsonuntil now: omitting a key is not the same answer as
writing it null, andfeint corpus --checkwent from 497 knowingly-accepted
divergences to 425 with the thirty-seven exemptions these retire. -
A block volume goes back to
availablewhen the server holding it goes, so
scw instance server deletereturns (#365). Reachable sincesbs_volume
was honoured: the CLI polls the volume the server's own answer named until it
settles, and nothing ever put the status back — measured at rc=124, never
returning, with five identical polls in the client's trace. #365 itself stays
open: making that volume the DEV1-S default, which is what the cloud does,
moves every root disk out ofinstance/v1where the whole server-volume
relationship is implemented, and that is a decision rather than a line.
Proved against
The clients
| Client | Version | Pinned in | Drives, in CI |
|---|---|---|---|
scw
| 2.56.3 | .github/workflows/conformance.yml (SCW_VERSION)
| Scaleway |
| Terraform | 1.13.3 | .github/workflows/conformance.yml (TERRAFORM_VERSION)
| Outscale, Scaleway |
| OpenTofu | 1.12.5 | .github/workflows/conformance.yml (TOFU_VERSION)
| Outscale, Scaleway |
octl
| 0.0.31 | .github/workflows/conformance.yml (OCTL_VERSION)
| Outscale |
exo
| 1.95.6 | .github/workflows/conformance.yml (EXO_VERSION)
| Exoscale |
The Terraform providers
The client above is the engine; what answers this emulator is the provider.
Each row is one required_providers entry, read where it is written.
| Fixture or stack | Provider | Constraint | What that is worth | Applied in CI |
|---|---|---|---|---|
tools/conformance/faults
| outscale/outscale
| ~> 1.7
| constraint: resolved fresh on each run, so the version that answered is not knowable here | yes |
tools/conformance/outscale/terraform
| outscale/outscale
| ~> 1.7
| constraint: resolved fresh on each run, so the version that answered is not knowable here | yes |
tools/conformance/outscale/terraform-doorway
| outscale/outscale
| ~> 1.7
| constraint: resolved fresh on each run, so the version that answered is not knowable here | yes |
tools/conformance/scaleway/terraform
| scaleway/scaleway
| 2.81.0
| exact: the version that answered | yes |
examples/stacks/exoscale
| exoscale/exoscale
| — | not pinned: whatever the registry served that day | no |
examples/stacks/outscale
| outscale/outscale
| ~> 1.7
| constraint: resolved fresh on each run, so the version that answered is not knowable here | yes |
examples/stacks/outscale/modules/net
| outscale/outscale
| ~> 1.7
| constraint: resolved fresh on each run, so the version that answered is not knowable here | yes |
examples/stacks/scaleway
| scaleway/scaleway
| 2.81.0
| exact: the version that answered | yes |
examples/quickstart/outscale
| outscale/outscale
| ~> 1.7
| constraint: resolved fresh on each run, so the version that answered is not knowable here | yes |
examples/quickstart/scaleway
| scaleway/scaleway
| 2.81.0
| exact: the version that answered | yes |
Read the third column narrowly, because it is the one a consumer pins against.
An exact constraint names the version that answered. A constraint is
resolved by terraform init -upgrade on the runner, every run, so the version
that answered is not knowable from this repository — it is a floor, not a
proof. Not pinned is not pinned: nothing here says which version ran, and
no number is invented to fill the cell.
The stacks the last column says CI does not apply are declared rather than
merely absent, so a no is a decision somebody wrote down and not a stack
nobody wired up:
examples/stacks/exoscale— suspended — no Terraform for Exoscale
until upstream exoscale/terraform-provider-exoscale#573 is fixed: the
published provider honoursEXOSCALE_API_ENDPOINTfor one of the two
clients it builds, so an apply splits between the emulator and a paying
account, and #525 measured five signed requests leaving for the real cloud
from afeint downon this stack (2026-08-26).feint uprefuses the
engine at the doorstep; the exo CLI drives the pack instead. What the
pinned fork had proved stays dated in docs/limits.md: 16 resources, empty
second plan, clean destroy, 2026-08-24.
Generated from the conformance workflow and the fixtures at this tag.
A newer client is not known to work until the pin moves and the suite
passes on it: docs/clients.md says what each cell is worth.