Added
-
Elastic Metal is listed:
baremetal/v1/API.ListServers, and the
catalogue the CLI joins to it,baremetal/v1/API.ListOffers(#631). The
product is not emulated and nothing else of it is: no offer is in stock,
nothing is ordered, installed or rebooted, and the thirty-five other
operations are declined with their reason. The listing exists because a
fleet inventory has to enumerate the product to describe a mixed fleet, and
it was measured before it was written:scw baremetal server list(2.56.3)
callsGET /baremetal/v1/zones/{zone}/servers?order_by=created_at_asc&page=1
and stopped on the 501 the unrouted prefix answered; the Python SDK's
list_servers_all(scaleway 2.12.0) pages withpagealone until a page
comes back empty. The issue sized the product at one route and the CLI's
source says two: once the servers answered, the same command read
ListOffersand printed the catalogue's name for each server'soffer_id,
so the catalogue answers one offer per distinctoffer_idamong the seeded
servers,stock: empty, nothing invented around the name the seed gave it.
A server enters through the state door (serve --state,
PUT /_feint/state) as Kindbaremetal/serverwith the SDK's field names in
Attrs, and the view answers the SDK's wholeServerfield set,
ips[].versionincluded, derived from the address when the seed did not
say. The product is under the drift gate and the contract (elastic-metal/v1,
the portal's slug for the SDK'sbaremetal), and thescwsuite seeds one
server and lists it. The answer for a zone outside the six the product
declares is this pack's convention, not a recording;docs/limits.mdsays
so. -
An operator who declares their catalogue gets their typos refused, and
nobody else changes (#126):feint serve --strict-catalog catalog.json,
andfeint startcarries it. The file names, per provider, the images,
templates and machine types the project allows; a create outside it is
refused in each cloud's own error shape, and so is the lookup the client
makes first, soscw,octlandexorender their ordinary
not-found path. What each pack refuses and in which shape, and which of
those shapes were recorded on a real account, is a table in
docs/limits.md:osc/Client.CreateVmsanswers the recorded
400/5023/InvalidResource;instance/v1/API.GetImage,
marketplace/v2/API.ListLocalImagesandinstance/v1/API.CreateServer
the recorded404 not_found;exoscale/v2.create-instanceand
create-instance-poolthis pack's404, unrecorded on a create. A kind
the file does not mention is not checked; a kind no pack checks is refused
before anything listens, because a line nobody enforces reads exactly like
one somebody does. Without the flag nothing moves: the compatibility mode
is byte-identical, the conformance suite runs in it, and
docs/limits.md's "identifiers are not checked" stays true word for word. -
An Outscale volume is
creatingbefore it isavailable, a snapshot
in-queuebefore it iscompleted, and a snapshot taken duringcreating
meets the measured409 InvalidVolumeState(#124), under
feint serve --consistency eventual. Measured against a real account on
2026-08-08:osc/Client.CreateVolumeanswersState: "creating",
osc/Client.CreateSnapshotissued before the volume settles is refused with
409 and code6007, and a snapshot is bornin-queuewithProgress: 0.
Here both settled at once, so that refusal could never fire, and a guard for
it had been tried and reverted for exactly that reason. #637's scheduler
makes the state reachable without a clock, one observation per read; the
guard reads the volume without observing it (Store.Peek), because the
first version consumed the very state it checked. TheDetailswording was
not recorded and is this emulator's; the status, type and code are the
measurement's, andosc.IsConflictclassifies the code as it classifies the
cloud's. With the mode off, nothing moves: a volume isavailableand a
snapshotcompletedat once, byte for byte, and a test holds that half. -
A backend gains and loses one server at a time (
lb/v1/ZonedAPI.AddBackendServers,
lb/v1/ZonedAPI.RemoveBackendServers, #676). Declined because the Terraform
provider reconciles a pool throughSetBackendServersalone, which is true
of Terraform and not of a Day-2 client, for whom adding one server is the
ordinary week-three operation. Measured on a real account on 2026-09-04 and
copied: an added address goes first in the pool; an address the pool already
holds refuses the whole request withinvalid_argumentsonserver_ip,
"server_ip x already exists"; a removed address leaves the others, and
removing one the pool does not hold answers 200 with the pool unchanged. The
order of several addresses added at once was not measured, and the handler
says so. Driven byscw lb backend add-serversandremove-servers. -
Four Load Balancer reads a Day-2 client asks for are served
(lb/v1/ZonedAPI.GetLBStats,lb/v1/ZonedAPI.ListBackendStats,
lb/v1/ZonedAPI.ListCertificates,lb/v1/ZonedAPI.ListSubscriber, #666).
Measured on both sides on 2026-09-04 with one generated Ansible collection,
39 of its 46 modules play against a real account and 28 here, and these
four routes were most of the difference.The two stats routes report on servers this emulator already owns, in a
backend it already serves:backend_idandinstance_idare identifiers it
minted,ipis the pool's,server_stateis the state of the server whose
NIC holds that address (stoppedfor an address nobody holds), and
server_state_changed_atis that server's own timestamp. What no route
reports is a health:last_health_check_statusis alwaysunknown, the
contract's own default for the enum and the literal truth, because nothing
here probes a backend — the Scaleway pack hands no balancer to the runtime,
and the runtime's balancing half reports what it distributes, never what
answered. A playbook assertingpassedlearns the limit from the value
rather than from a 501. The two lists are empty because that is what a
balancer here holds;CreateCertificateandCreateSubscriberstay
declined, and their reasons say so.Driven by
scw lb backend list-statistics,scw lb lb get-stats,scw lb certificate listandscw lb subscriber listin the conformance suite. -
A Day-2 leg: a month of an operator's writes on one standing stack, each
read back, then the platform proved working again (#673).mise run conformance:day2bringsexamples/stacks/scalewayup on its own emulator
and plays the catalogue intools/conformance/day2lib.sh: twenty-four steps
in do/undo pairs (rename, retag, protect, the address's tags and reverse, a
public address created, attached, detached and deleted, a rule added and
removed, the group's default opened and closed, the private network, the
VPC, the balancer, the frontend, a backend server added and removed, the
volume, a scratch volume and a snapshot created and deleted, a user data
key, the placement group, the gateway, the ssh key, a NIC joined and left,
and a reboot), fifty-odd writes, and after every one a read that has to
say the change happened. A 200 is not evidence: the comparator refuses a
wrong value and an absent path, the reboot's first read must not say
running, and every wait is bounded and says how long it waited.Then the platform: under a runtime, platform-web-0's own table is compared
with its capture from before the catalogue (#671's comparator, reused: a
net-zero catalogue is exactly a comparison of shape after mutations), the
emulator's own verification counters are read (#670), and the stack gate
replays its verdicts on the same stack throughFEINT_STACK_UP, fifty
writes older. With no runtime the leg plays the control-plane half and
says, by name, what it did not prove.What the leg found on its first runs, recorded rather than asserted, and
none of it fixed here. With no runtime:PATCH /ips/{id}with"reverse": nullanswers 200 and the read still carries the name, so the undo uses
"";POSTandDELETEon/backends/{id}/serversanswer 501 wherePUT
serves the list. Underincus-ovnon 2026-09-04, playing the catalogue step
by step on platform-web-0 and diffing its table after each: detaching a
second public address took the first one's/32off the guest (the re-plug
repair restores the pinned address and not the external routes that stay),
found only at the next join, whose replay put it back; and the reboot put
the migrated/32back on the routedeth0, from the guest's own netplan,
which the layer's reboot comparison reported asrestart(eth0)and its
one repair did not undo. The third difference was the instrument's: the
three private aggregates stood twice in the table, at the lease's metric
and at the driver's, and both readers now collapse a route held at two
metrics into one. The leg stays red under a runtime until the two defects
are fixed, and says which line. -
The runtime suite looks at the machine's own table across a restart, and
names the line (#671).tools/conformance/functional.shcaptures, from
inside the machine and before anything is restarted, its addresses and every
route with a next hop, normalised by two readers infunctionallib.sh
(connected routes, the link-local block, metrics,proto, the loopback and
scope-link addresses never enter); after the provider's own reboot verb, and
again after a stop and a start, it captures again and diffs. A line that
changed fails the stack with both spellings printed, before and after: the
cause where the effect is, instead of "nothing answers at 203.0.113.4:443"
sixty seconds later at the far end. A capture that failed is "cannot look",
never a side that compares equal to something.fnl_shape_reader_controlruns before any verdict, like the other reader
controls: it normalises two planted tables and requires the comparison to
fail on a planted difference, the regression one line apart; and a test
stubs the comparison to pass everything and requires the control to notice.
Six execs on a leg of 590 s. -
A reboot compares the shape after with the shape before, and needs no
table of expected values for it (#669). Nothing moves in the control
plane between the two, because the reboot holds the per-target lock from
one end to the other; so what the machine carried while it was up is what
it must carry once it is back, interface by interface (network, kind,
addresses, rule sets, as sets), the default route by its door, the rest of
the table as a set. The claims ride the same reading as the plan's own, so
a reboot is read once and repaired once like any other boot. On the
regression of 2026-09-04 this answers, at the moment of the reboot,
restart(default route) want via 169.254.0.1 dev eth0, got via 10.77.0.1 dev eth1, where the runtime suite used to wait sixty seconds two steps
later to say "nothing answers".A before that could not be read is said, in a
WARN, and the reboot is
judged on its plan alone rather than on a guess; a machine that was not up
has no before. A poweroff then a poweron as two API actions is not a
reboot: a NIC attached to a stopped server legitimately changes the shape,
and there the derived claims judge. -
A verdict somebody reads: the verification is published, and the runtime
leg gates on it (#670). After every lifecycle door — a boot, a reboot, a
hot join, a hot route, the late-address door of a virtual machine — what the
runtime read back is published where a human looks and where a gate fails:
anERRORline per broken claim naming the claim, the wanted value and the
one found;Runtime["verified"]on the resource, out of every client's view
and on/_feint/statefor whoever asks; and four counters on
/_feint/health,verification.{held,broken,unreadable,repaired}, which
moves the health schema to version 8.The API state does not move. A machine running with the wrong door is
running and unreachable, and publishing it stopped would be the lie in the
other direction. One bounded repair: a broken first reading replays the
post-boot order once, every step of it idempotent, and reads again; the
second reading is what is published, andrepairedcounts how often the
first was wrong, because a repair that fires on every boot is a defect
hidden behind a retry. An unreadable reading is never replayed onto, and the
late-address door reads only while nothing readable was published, so a GET
never execs into a verified machine.tools/conformance/guard.sh verification <endpoint>, called by every leg
before its emulator stops:broken > 0fails the leg naming each claim from
the state, more unreadable than held fails it as a pass that measured its
own blindness, absent counters fail it rather than read as zero, and a pass
with no runtime is told so rather than judged. -
The runtime reads back what a machine actually carries, and every claim
of its plan is answered with three outcomes (#668). The Incus driver
implementsObserver: onequeryfor the machine's own devices, one
network getper emulated network it sits on, and three execs into the
guest for the addresses, the table and, per door a claim asks about,ip route get <to> from <from>. The text of bothipimplementations is parsed
(iproute2 on Debian, Ubuntu and the RHEL family, busybox on Alpine), and what
does not compare never enters theShape: connected routes, the link-local
block, metrics,proto,src, loopback, the operator's profile devices and
their networks' gateways.held,broken,unreadable, never two: a read that failed answers
unreadable on every claim, counted, and a runtime with no reading half is a
fourth state, not asked, so "nobody looked" cannot pass as "nothing was
wrong". The rule sets a machine wears are claimed on its filtered interfaces
from the same fields the firewall step reads, bare on the networks a pack
declared outside its groups' reach, and not at all on a host that withdrew
the firewall capability.Two controls hold the instrument, both required by
measurement-integrity:
a divergence planted one digit off is reported as itself, and a failed read
is neither held nor broken. And the regression of 2026-09-04 is reproduced
through the fake runtime: the reply from the public address leaves through
the private gateway, and a door claim is broken with both doors named. The
door comparator exists; nothing derives a door claim yet (#672). Publishing
the verdicts is #670, comparing a reboot's before and after is #669. -
A plan that claims the default route twice is refused before the runtime
is asked for anything (#667).Reconciler.Expectderives, from the
declared plan, who claims which property: a public address owns the way out,
Plan.Egressclaims it,NoEgressclaims it, and a property with more than
one claimant refuses the boot with every claimant named, publishing the
pack's own failed state the way a missing plan does. On the hot doors (an
address routed, a network joined) the step that would execute the
contradiction is refused instead, and the route the machine has is left
alone.This is the layer's guard for #660: on 2026-09-04 the egress step replaced,
withip route replace, the default route the routed address had laid, every
exec returned 0, and the machine answered nothing at its published address.
The pack-side fix stands; this is what stops a fourth pack, or a change in
this one, from writing the same contradiction and no test moving.What a consistent plan claims is returned as typed claims, each with its own
tolerance (the mask only when the plan said one, any address of the block for
a DHCP lease, the aggregates via a gateway of the plan), answered against a
Shapewith three outcomes: held, broken, unreadable. Which door a reply
leaves by for a public address is a measurement and one shape has none
(#672), so the derivation states no value for it: silence, not a guess. The
reading that fills aShapefrom the runtime is #668. -
Flexible GPU is served, the whole family (#619):
osc/Client.ReadFlexibleGpuCatalog,
osc/Client.CreateFlexibleGpu,osc/Client.ReadFlexibleGpus,
osc/Client.UpdateFlexibleGpu,osc/Client.DeleteFlexibleGpu,
osc/Client.LinkFlexibleGpu,osc/Client.UnlinkFlexibleGpu.A published training course spends a whole page on this product, and a reader
following it against the emulator met six 404s in a row. On the control plane
an fGPU is the shape of a volume rather than of a machine: what a client
observes is its model, its subregion and whether it is attached, and all three
are records rather than hardware. That is the line the Load Balancer already
sits on, where the product is served andGetLBStatsanswers what the emulator
holds and no health, because nothing here measures a backend.The catalogue is a reading of a real account, eleven models field for field, so
the create can refuse a model the cloud does not offer and a generation the
model is not compatible with. The whole family or none of it: a catalogue whose
items no create takes is the menu theListVolumesTypesargument refuses.Dedicated hosts, VM templates and VM groups keep the refusal they had, with the
argument narrowed to what it still covers. -
The Load Balancer offer table is served (
lb/v1/ZonedAPI.ListLBTypes,
#658). Four types read off a real account, paged, and driven byscw lb lb-types listin the conformance suite.It was declined because "no measured client reads it before creating". A
generated Ansible collection now does: it writes an_infomodule per list
operation, and answering what is on offer is that module's whole purpose.The issue also reported the emulator's offer as closed and enforced. It was
not: the refusals in the report came from one flexible IP reused across three
creates, and with a fresh IP each timeLB-INEXISTANTwas accepted like the
rest. Closing the offer was written and reverted, because the committed corpus
replaysCreateLBwith a redacted type and a value check refuses the replay
itself.lbtypes.gocarries the whole argument, and it bounds every future
refusal of a value: a pack may only refuse on what the anonymiser preserves. -
serve --consistency eventual: a client's waiter finally has something to
observe (#637, and the mechanism #124 asked for). Off by default. That governs the ordinary
chains alone: since #654 a chain that is an action's only signal — a
reboot'sstopping, starting, running— is walked in both modes, or a
reboot would be invisible to every client that did not ask for the flag.A reboot's target state is the state it started from, so
state == "running"
proves nothing to a client waiting on one. Without task support the only signal
available is watching the machine leave its initial state — and this
emulator answeredrunningfrom the first read to the last. Sopoweron,
poweroffandstop_in_placecould all be exercised here, andreboot, the
one action where waiting is subtle, could not be exercised at all. The author
of #637 had to prove their own waiting guard by unit test, because running it
against feint failed for a reason that had nothing to do with their client.With the mode on, a Scaleway server walks the states
fr-parwalks, measured
2026-09-03 by polling a real DEV1-S through each action:action states answered, in order poweronstarting, thenrunningrebootstopping,starting, thenrunningpoweroffstopping, thenstoppedStates advance on reads, never on a clock, and that is the design rather
than a shortcut: the emulator's clock is injected, so a transient state timed
on a wall clock would either never end under a frozen test clock or turn every
suite into a wait. A four-second suite stays four seconds.Two consequences worth knowing. An action is not an observation — the read
a lifecycle action makes in order to change a resource does not advance it, or
the action would consume the first state it just pushed (that defect happened,
and the reboot answeredstarting, runningwhere the cloud answersstopping, starting, running). And a failed action walks no chain: a start that
failed answers its failed state, because narrating a path towards arunning
it never reached is the plausible-wrong answer this project exists to avoid.The Outscale
creating → availableandin-queue → completedchains, and
the409 InvalidVolumeStaterefusal that was reverted for want of a
reachable state, followed on the same mechanism (#124, below). -
A project is a register now, and
account/v3can write to it (#391).
account/v3/ProjectAPI.CreateProject,account/v3/ProjectAPI.UpdateProject
andaccount/v3/ProjectAPI.DeleteProjectare served —POST /account/v3/projects,PATCHandDELETE; a project that
still holds a resource refuses its delete with412 precondition_failed,
resource_still_in_use, which is the cloud's own answer and the reason the
route is worth serving rather than answering204over resources that outlive
it.It exists because of the refusal below: refusing a create that names a project
nobody holds, with no way to make one real, would have left the multi-project
behaviour this emulator was built with no door at all. The door is the one the
cloud has.--projectstakes an identifier,name=<uuid>, and an entry with no=
keeps deriving one from the name as before. This is what keeps #372's case
alive: a stack carrying a production project id used to work because
GetProjectechoed anything, and it now works because the operator states it.
A declaration is not an echo, which is the distinction #83 closed on all three
packs.
Changed
-
Five Exoscale reads stop claiming no client drives them (#644, found by
#631). Each carried aRoute.Undrivenreason written aboutexo, and four
of them named their own expiry condition in the same breath: the Terraform
provider does read by id, and it did not count because the build reaching
this emulator was the patched forkdocs/limits.mdpinned, "a client this
project patched is not the official client". The published v0.71.0 drives
this pack since #644, so the condition was met rather than argued away, and
get-block-storage-snapshot,get-elastic-ip,get-instance-pool,
get-load-balancerandget-operationare driven. The last one is the
interesting case: its reason ended on a sentence that turned out to be a
prediction, that the route stays mounted because the day a client polls it
must find it served rather than404, and the provider polls every operation
the SDK returns, terminal or not.docs/routes.md's "served, and driven by
no client" list falls from 18 to 13 for this pack, and the reasons that stay
are still aboutexo, still measured, and now the only client that reaches
them.Nobody would have noticed: the register
coverage/evidence.jsonhad not been
regenerated since #683, so it still carried figures from before that provider
ran, andTestEveryUndrivenOperationSaysWhyhad nothing to compare against.
#631 regenerated it for its own two rows and the five stale reasons came out
with it.
Fixed
-
A private machine behind a Public Gateway reaches the Internet under
--vm incus-ovn(#647, #655). A machine with no public address had no
outbound connectivity at all, andpush_default_route = trueon the gateway
attachment installed no route: measured on a stack of 37 resources that
Terraform applied, re-planned empty and destroyed cleanly, whereaptthen
failed on every private machine with "Failed to update apt cache after 5
retries". The shape only shows when a client provisions a machine rather
than starting one, which is why a green apply said nothing about it.What the network announces is now the split the cloud makes: the OVN network
announces where the private fleet is and stops announcing where the Internet
is (ipv4.dhcp.gateway=none), and the way out is laid per machine by
Plan.Egressfor the machines the pack says are entitled to it. A server with
no public address and no gateway reaches its VPC and nothing beyond it, which
is what the real cloud does and what a DHCP-announced default route made
impossible to express. -
A poweron that failed no longer narrates
starting(#738). Under
--consistency eventual, a start whose runtime call failed answered
[starting stopped stopped stopped]across four reads, with a task saying
success: a client watching the action saw a boot that never happened.
transitionTorefused any settled value outsiderunning,stoppedand
stopped in place, and this pack's failed state isstopped— Scaleway
declares no error state for a server — so the guard passed it. An action that
settles where it started made no journey and now narrates none; a reboot,
which legitimately returns to where it began, says so through
transitionBackToand keeps its chain. This makes true the sentence #637's
entry above already claims.The test is the part worth reading.
TestAFailedActionWalksNoChainlisted
the failed statesfailed,starting,unknownand"", none of which
this layer produces, while its accepting half required a chain for
from == settled == "stopped"— the failed poweron itself. It asserted the
defect in one half and missed it in the other, over a population that
excluded the only value that occurs. The accepting half now names journeys
(stopped→running,running→stopped) rather than destinations against one
origin, andTestAFailedPoweronWalksNoChaindrives the whole path through
HTTP, because the unit-level guard was already believed to hold. -
A machine whose public address moved onto its private NIC carries the
same shape before and after the reboot verb: the driver stops competing
with systemd-networkd for the routed interface and takes it over (#742). The stack gate's reboot comparison (#671) reddened on
platform-web-0on the runner as on the author's station, and in two
mirror shapes: oncebefore: addr eth0 203.0.113.4/32, onceafter: route 0.0.0.0/0 via 169.254.0.1 dev eth0. Polled at half-second intervals across
the reboot on 2026-09-07, the reboot itself was deterministic: netplan lays
eth00.5 s after the container is back, the driver takes the stale
address off 2.2 s after, the driver lays the default route 2.8 s after.
What varied was the state before it. The migration of #548 edits the routed
device, which re-plugs it, then repaired the interface at once and deleted
the moved address right after, racing systemd-networkd's re-application of
the guest's own netplan to the new link: networkd last, the machine kept
the address on two interfaces; the driver last, it kept none oneth0and
no default route. An order was tried first and did not hold on the runner:
polled at 100 ms across the hot attach, the re-plugged link answers
Network File: n/afor 150 ms before networkd matches it, which reads
exactly like "nobody manages this", and while networkd owns the interface's
configuration every order the driver takes is a race. So the driver now
tells networkd, in networkd's own configuration, to let go of a routed
interface whose address moved: a drop-in beside the unit netplan rendered
([Link] Unmanaged=yes, the mechanism #702 already uses for the resolver),
a reload, and a wait for the state networkd reports,(unmanaged), before
the stale address is taken off and the routes laid — before the re-plug on
the hot path, at the first boot after a cold migration on the restart path,
and at every boot after that nobody but the driver writes the interface. A
guest with no networkd is left alone. What is produced is deliberately
unchanged, an addressless routed interface carrying the default route
through169.254.0.1; #742 changes who writes it, not what is written, and
whether that is the right shape is #695's outbound half and the ADR of
#726, not decided here. The defect is older than v0.12.1: today's gate driving a
binary built from that tag reaches the step and fails it too, by more. The
comparison that finds it is later than that tag, so no earlier release was
measured on this step. -
A machine holding a public address and a private network answers at its
published address again underincus-ovn: the network names its own
gateway as the lease's resolver (#697).runtime-proofwas red four
scheduled nights (09-04 to 09-07) onplatform-web-0of
examples/stacks/scaleway,waiting: platform-web-0 answers on port 443,
while the control leg at v0.12.1 passed each of them. The request landed
(one connection inSYN_RECVinside the guest); the reply died on an
on-link/32towards10.209.83.1, the uplink's address, which is what
the station dials from and is not on the guest's segment. That route is
the guest's own,RoutesToDNS=towards the DNS server its lease names,
and since #693 the lease named none, so Incus named the uplink's address
for it: naming nothing was not "no route". Three values measured, two
rejected:RoutesToDNS=noin the guest's drop-in changes nothing, the
lease lays the route before the drop-in lands; a public resolver repairs
the reply and moves the dead/32to1.1.1.1, which is #684 back. The
gateway is on the segment by construction: routes naming the uplink0,
no dead/32elsewhere, the replyvia <gateway>, and the station reaches
the published address. Nothing answers DNS at the gateway, and nothing has
to: the name server a machine uses comes from the drop-in andresolvectl
(#694, #696), and the lease's value decides only which route is laid.
TestAnOVNNetworkLaysNoRouteTowardsItsResolverholds the invariant rather
than a value — every name server the lease names is on the segment, and it
names one. The outbound half of #695 is not this: a machine holding a
public address still has no default route under OVN. -
osc/Client.ReadVmsrefuses aVmIdsvalue that is not an identifier
(#396), as the real account does and as no other read does. The recording
of 2026-08-21 (corpus/outscale/oapi-cli-refusals.jsonl) sent
["not-an-identifier"]to seventeen reads:ReadVmsanswered 400 with
Errors [{Code 4104, Type InvalidParameterValue, Details "the provided value does not respect the expected ID prefix"}], and the sixteen others
answered 200 and an empty list. This emulator answered 200 on all
seventeen. It now refuses onReadVmsalone, with the recorded code, and a
test sends the same value to every declared identifier filter of every
other read,ReadVmsState'sVmIdsincluded, and holds their 200: the
asymmetry is upstream's, and reproducing it on one read is the fix, on
seventeen it would be sixteen new divergences. The two corpus exemptions
naming this issue are gone. -
An Exoscale validation refusal carries its
errorsarray (#397).
exoscale/v2.create-private-networkandexoscale/v2.update-private-network
refused a range declared backwards, or half declared, or a create with no
name, with{"message": …}alone, where the recording of 2026-08-21
(corpus/exoscale/exo-refusals.jsonl, a real ch-gva-2 account) shows
{"message": …, "errors": [{"message": …}]}on both recorded 400s. The
same recording shows five 404s and one 409 without the array, so it is the
shape of a validation failure and not of an error, and it is written on
those refusals alone: a 404 still carries the message and nothing else,
and a test holds that half. The corpus exemption naming this issue is gone,
andfeint corpus --checknow compares the two recorded exchanges rather
than excusing them. -
osc/Client.ReadImagesandosc/Client.ReadSnapshotsserve
Filters.AccountAliases(#700), the filter the contract's ownReadImages
example uses. Both reads refused it with the 4001 that names what they
serve, measured on 2026-09-05 through osc-sdk-python 0.42.0 and reproduced
on 2026-09-06 before the fix. It selects on the owner's alias, and the owner's alias is
now on every object the owner holds: the catalogue's images carried
AccountAliassince #95 and a registered image or a snapshot of the same
AccountIdcarried none, so the filter would have split one owner in two.
TheSnapshotschema declares the field besideAccountId, and the real
cloud puts it on an image whose owner has an alias (shapes/outscale.json).
What stays, anddocs/limits.mdrecords: the catalogue is the emulated
account's, by decision, soAccountAliases: [Outscale]answers an empty
list here rather than a catalogue relabelled with an owner id the corpus
redacts, andselfis not an alias this API was measured accepting. -
An Exoscale elastic IP reads its labels back (#703).
exoscale/v2.create-elastic-ipandexoscale/v2.update-elastic-ipdeclared
labelsin their request structs and stored nothing, and no view served the
field, soexoscale/v2.get-elastic-ipandexoscale/v2.list-elastic-ips
answered a labelled address without labels while its description, from the
same body, came back fine, and the second plan of a stack that labels its
address was never empty: measured on 2026-09-05 with the Python SDK, and
under provider 0.71.0 asPlan: 0 to add, 1 to changeon the one resource
of twenty-three carrying labels.
Stored on create, replaced on update when the client sends the field and
left alone when it does not, cleared by an empty map, served when non-empty
and absent otherwise, the way the private network already did. The same
pass narrowedexoscale/v2.reset-elastic-ip-fieldto what the API description declares,
descriptionalone: the pack acceptedlabelsandhealthcheckthere,
neither in the contract's enum, and the first answered 200 while clearing a
field it had never stored. -
feint env exoscaletells a Terraform user what the emulator does today
(#701). The note on stderr still said, a day after #644 had lifted the
refusal, that the provider honoursEXOSCALE_API_ENDPOINTfor half of itself
and that Terraform must not be pointed here — the first thing a Terraform
user reads, telling them the opposite of what the run then did. It now says
the published provider drives this pack from v0.71.0
(exoscale/terraform-provider-exoscale#576), to pin at least that version,
and that an older one is refused by user agent, naming the floor; it reads
the floor from the constant the refusal reads, so the two cannot drift apart
again. The same stale sentence was measured in four documents and one guard,
and all say the same thing now:docs/confidence.mdanswered no to a
Terraform run against Exoscale,docs/environment.mdandexamples/stacks/
saidfeint uprefuses the engine at a doorstep #644 had removed, and
emulator.envstill refusedFEINT_EXOSCALE_ALLOW_TERRAFORMby name — a
variable nothing reads since #644, whose refusal #644 itself recorded as
gone. It is gone now. -
reverse: nullclears a flexible IP's reverse, and reads back null (#676).
The SDK sends the reverse as aNullableStringValuewhose JSON is a literal
nullto clear, and the emulator read it into a*string, where a null and
an absent field are the same thing: the write answered 200 and the reverse
stayed, the shape of #654 one product over, found by the Day-2 leg's
read-after-write. Measured on a real account on 2026-09-04: a fresh address
readsnull,{"reverse": null}answers 200 and readsnull, and so does
{"reverse": ""}. What the account also answers, and this emulator does
not: a reverse that does not resolve is refused with a constraint on
reverse(docs/limits.mdkeeps that divergence, since a stack's reverses
point at documentation addresses nothing resolves). -
A re-plug of an OVN NIC gives the guest back every public address the
device still routes to it (#675). Attaching a second public address to a
server holding one, or detaching it, re-plugs the NIC — the route keys are
not live-updatable — and the repair that follows put back the pinned
private address alone: measured on 2026-09-04 underincus-ovn, on the
device and in the guest at each step, attaching203.0.113.5to a server
holding203.0.113.3left the device routing both and the guest carrying
the new one alone, and detaching it left the guest with no public address
at all. The next join replayed the addresses and repaired it in silence,
whichrepaired: 1on/_feint/healthwas the only trace of. The repair
now reads the device after the edit and re-adds every/32of
ipv4.routes.external: an attach reads the merged list, a detach the kept
one. The routed NIC's path was not the defect and is held by a test of its
own, so nobody reasons about it again. -
A restart reconciles the routed NIC to its device, and the guest's own
config keeps laying the launch shape (#674). A server created with a
public address and no network boots on aroutedNIC (#202), declared
statically in the network-config the launch hands cloud-init — the address
as a/32, the on-link default route through169.254.0.1. cloud-init
renders it once, at the first boot, into the guest's own
/etc/netplan/50-cloud-init.yaml, and that file outlives every edit the
driver makes to the device afterwards: measured on 2026-09-04 under
incus-ovn, a server whose private NIC arrived hot and took the address
with it (#548) still declared203.0.113.3/32oneth0in the guest while
the device pinned nothing, and an API reboot put the address back oneth0
beside the one the driver had laid oneth1. Two writers of one interface,
now in a fixed order: the guest's config lays it at every boot — and must,
measured the same day: a first attempt that tooketh0out of that config
left systemd-networkd managing nothing,systemd-networkd-wait-online
waiting to its 120 s ceiling and sshd starting 90 s after the ssh suite had
given up, where main logs in within twenty seconds — and the restart path
then waits, bounded, for the guest to have laid the interface, takes off
every address the device neither pins nor routes, and puts back what it
does, the link-local next hop and the default route through it. -
An OVN network announces a public resolver, never the uplink's address
(#660). Incus announces the uplink's own address as the DNS server, and that
address is also the source the station dials a public address from once the
address rides an OVN NIC (#548); inside the guest, systemd-networkd's
RoutesToDNS=lays an on-link/32towards every DNS server the lease
names, more specific than the aggregates towards the router, and the reply
to the station ARPs on the switch for an address nothing there carries.
Measured 2026-09-04 underincus-ovn:platform-web-0served 443 inside
and answered nothing at203.0.113.3:443;ip route del 10.209.83.1 dev eth1turned the dial from 000 into 200,ip route addturned it back. This entry was written when the network announced a public
resolver; it moved twice afterwards inside this same release. #684 and
#693 took the announcement out, because a public resolver is off the
subnet and the on-link /32 towards it took the machine's way out with it.
Announcing nothing then let Incus fall back to the host's resolvers — the
uplink's own address — which is the dead route this entry set out to
remove, and #697 settled it by announcing the segment's own gateway: on
the subnet, so the route it lays is alive. The resolver itself reaches the
guest throughresolvectland a drop-in (feint serve --resolver,
default1.1.1.1), and the uplink's address is refused whatever the field
says.RoutesToDNS=is the guest's ordinary mechanism, not a defect worked
around: the announcement removes the collision that made it harmful. Not
established, measured the same day: that a machine with a way out resolves
through the announced resolver — the announcement was on the wire and the
guest'sresolvedheld no server on any link, resolving once one was set
by hand; a defect of its own, followed apart. Left open on #660: a machine
with a public address and a private NIC still comes up, in some orders,
with no default route at all (overlaps #678).feint serve --resolverand
feint start --resolverare new flags: CLI surface version 24. -
A snapshot of a disk nothing was ever attached to was accepted, and the
published example stack depended on it (#650, #646).The cloud refuses it:
cannot create a RO disk from an empty disk. So the
golden-image pattern the Scaleway example stack demonstrates — a volume, a
snapshot of it, an image cut from the snapshot — could not apply against the
real Scaleway API, and the reporter found out by running their copy of it
there.Where the line falls was measured three ways, because it is not where it first
looks:subject fr-para volume created and never attached 400, cannot create a RO disk from an empty diskthe root disk of a server that never started 201 a disk detached from a deleted server 201 So the question is not whether the machine ran, and not whether the disk is
attached now. It is whether the disk was ever anybody's — which is why the
mark is set on attach and never cleared on detach.The refusal carries a
detailsentry with a reason and a help message and no
argument_nameat all, soArgumentErroromits that field when it is empty
rather than sending"".The example stack now builds its golden image from a disk a builder machine
carries, which is what a client has to do against the cloud, and the pattern
it teaches survives contact. -
The Scaleway example stack said one thing and did another, and was not
routable as written (#646). Itsvpc_routewas commented "reach the
management range through the admin network" and pointed atapp— and the
code was the half that was right: a route's next hop is a private network of
its own VPC, andadminis in the other one.Underneath that, the topology cannot carry what the header advertises: two VPCs
that share nothing cannot reach each other, so the bastion described as "the
only public door" opens onto nothing. Undermode: offnothing boots and the
difference never shows — the reporter met it by running Ansible through that
bastion under--vm incus-ovn, and spent a while suspecting their own
ProxyCommand.The header says so now, in the file a reader copies rather than only in the
feint.yamlbeside it. Making the stack routable — one VPC, several private
networks — is deliberately not done here: it would trade the isolation this
stack demonstrates for reachability it does not, and that is a choice about
what the example is for. -
Two inputs the cloud refuses and this emulator accepted, and one it accepts
that this emulator refused (#648, reported with a differential run: the same
Terraform stack againstmainand againstapi.scaleway.com).The reported pair, both in the direction that costs — a client green here
learns nothing about the real cloud:input fr-parbefore an ACL rule whose dst_port_highis belowdst_port_low400, and it names both bounds 200 instance/v1 CreateSnapshoton a block (SBS) volume404 not_found,resource: instance_volume201 The first is not an exotic input: it is what the Terraform provider sends for
the commonest rule there is, one port opened, since a rule that names only
dst_port_lowleaves the high bound at zero. Three rules out of three in the
reporter's stack were that shape.The second is the golden-image pattern. A server's root disk lives in
block
since #365 — which is what the cloud gives a DEV1-S — and a block volume is not
aninstance_volume, so instance/v1 does not snapshot it. This pack resolved
both products there on purpose (#571) and the measurement reverses that: the
reason it was written has been served differently since, and the conformance
suite had already stopped taking that route.Measuring it turned up two more divergences on the same route, and one goes
the other way:argument_nameisrules[0]dst_port_high— no dot before the index, none
before the field. This pack wroterules.0.dst_port_high, wrong for every
rule error rather than for one of them. Thereasondiffers per field too:
unknownforactionandsource,constraintfor the ports.- an unknown protocol is normalised to
ANY, not refused.NONSENSE
answers 200 and reads back asANY. This pack refused it — the rarer
direction, stricter than the cloud, and a client that would have worked
againstfr-parmet a 400 here. Being stricter is still being different.
One detail is copied rather than tidied because nobody would invent it: in the
same body, the high bound'sminis the string"443"and the low bound's
is the number0. -
The
organizationfilter was ignored, so an organization that owns nothing
here answered the whole fleet (#638). Seven ofListServers' eight declared
filters partitioned the set exactly,projectincluded and with the same
shape; only this one had no effect.The failure mode is what makes it worth a line: an ignored filter does not
answer an error and does not answer nothing. It answers a well-shaped,
plausible, larger page, and the client cannot tell. It is #630's defect
mirrored — match everything where that one was match nothing.Measured on fr-par, 2026-09-02, with one volume on the account:
request fr-paranswers?organization=<the account's own>the volume, visible ?organization=<a ghost uuid>200, empty ?organization=not-a-uuid400 invalid_arguments,argument_name: organization, "not-a-uuid is not a valid UUID."Both facts were missing: the filter partitions, and a value that is not a UUID
is refused rather than accepted and ignored.Why this was left alone until now, and what changed. The reason is written
inlistSSHKeysand it was a good one:scwnames its configured organization
on every list, nothing obliges that configuration to spell this emulator's
constant, and comparing once told the CLI that the key it had just created did
not exist. #391 settled the same argument for projects — the cloud filters, and
a client configured fromfeint envis clear of it.feint env scaleway
publishesSCW_DEFAULT_ORGANIZATION_IDas this emulator's own constant, and the
conformance suite'sfake-credentials.envcarries the same value. -
A package install ran under the cap made for a control command, and the
nightly runtime proof died on it (#641). Everyincuscall went through one
120-second cap — the right bound forinfo,startornetwork create, where
a control plane slower than that is broken rather than busy.incus exec <builder> -- dnf install -y -q openssh-serverwent through it too, and on
2026-09-03 it was killed at exactly that limit while buildingalmalinux/9,
taking the whole scheduled run with it. The night before, the same command had
finished just under.A step inside a build instance now runs under its own cap, ten minutes, and the
build as a whole is still bounded by the twenty minutes that stop a wedged
download from holding the lock for ever. An operator who setTimeoutbecause
their station is slow gets both scaled, since a station that needs a longer
control cap needs a longer install cap by the same measure.The cap is asserted rather than described: the deadline is set before the
runner seam as well as before the real binary, so a test reads
ctx.Deadline()and says which cap a call ran under. Both halves are held —
a guard that gave everything ten minutes would break what the control cap is
for. -
Two refusals answered the wrong status, and a client branches on exactly
that (#394). A 404 says "create the parent first", a 400 says "your body is
wrong and the parent is beside the point", a 403 says "you are not allowed
here", and Terraform retries none of the three the same way.vpc/v2 CreateRouteresolved the VPC before it read the body. Measured on
fr-par, 2026-09-02, with avpc_idthat names nothing:request fr-paranswersno next hop at all 400, argument_name: route,requires_one_of: {"fields": "nexthop_vpc_connector_id, nexthop_private_network_id"}with a next hop 404 resource is not foundno vpc_idat all400, argument_name: vpc_id,help_message: "uuid: {}"So the cloud is not validating the body first as a habit: its generated layer
enforces what the document declares — a required field, a UUID shape, a
requires_one_of— and only then does a handler resolve anything. The same
request answers 400 or 404 depending on which of those two walls it meets, and
this emulator answered 404 to all three.lb/v1 CreateRouteanswered 404 for a frontend that does not exist, where
fr-par answers 403 — and that is this route's own answer rather than the
product's:GET /frontends/{id}andGET /lbs/{id}both answer 404 on both
sides. The status is now the cloud's.One divergence stays, and it is written down rather than left implicit.
lb/v1sends noscwerror envelope on any of those three refusals: no
type, nodetails. This emulator sends one, because
contracts/scaleway.jsondeclares a singleerrorSchemafor the whole
document —scw.ResponseError,required: ["type"], extracted from the SDK —
andinternal/probevalidates every refusal against it. Lifting that means an
errorSchemaper product, arbitrated by a recording rather than a document.
docs/limits.mdcarries it. -
A create naming a project that does not exist was accepted here and refused
by the cloud, across seven products (#391). Recorded in #390 and re-read
field by field on 2026-09-02, because the corpus redacts values and the words
are what a client branches on:request fr-paranswersinstance/v1create (server, IP, security group, placement group)403 permissions_denied,details: [{resource: project, action: read}]lb/v1create403, details: [{resource: loadbalancer, action: write}]block/v1CreateVolume403, details: [{resource: volume, action: write}]iam/v1alpha1CreateSSHKey403, details: [{resource: ssh_key, action: create}]vpc/v2,vpcgw/v2,ipam/v1creates404 not_found,resource: projectTwo shapes, not seven, and what varies inside the 403 is a field rather than a
dialect: onlyinstance/v1names the project at all. The others name their
own product and the action refused, which is a different sentence.GET /account/v3/projects/{id}stopped echoing, and that is a documented
decision reversed rather than an oversight fixed. It answered for any
identifier so that a stack carrying a production project id would not die on
the one call that has nothing to do with what it is testing (#372). An echoing
read beside a refusing create is worse than either alone — the client resolves
a project with 200 and is then refused every resource under it, which is the
disagreement #369 had between its own create and list, one product out. The
reason the echo existed did not disappear: it moved to--projects name=<id>.Two ordering defects surfaced with it, and the corpus named the first one.
vpcgw/v2 CreateGatewayvalidated the gateway type before the project and
answered 400 where the recording says 404;ipam/v1 BookIPvalidated its
source first and answered 400 where fr-par answers 404 on the project. Both
now ask about the project first, which is the order the cloud answers in.And the replay had to learn something to be able to judge any of this. On
the account these corpora were recorded from, the organization and the default
project carry the same UUID, so the redaction gives both
00000000-…-000000000001, and the replay was sending the emulator's
organization where a project belonged. It now seedsreplay.Options.Bind
per file — the seam that already existed for standing one account in for
another — and only for a file whose recorded project the cloud actually
accepted.scw-refusals.jsonlgets no stand-in, so its ghost project goes out
as itself and is refused, which is the one thing the gate has to be able to
judge here.The 36 acceptance entries naming this issue are deleted, and one more went with
them:ipam/v1/API.ListIPshad stopped excusing anything the moment #369
landed, and this replay is what reached it. -
A server created under a named project was invisible to the list that
followed it (#369).createServerhonoured theprojecta request named,
and every unfiltered list scoped itself to the default project, so the create
and the list disagreed: a client that creates then lists — which is every
client — read an empty fleet. The comment abovescopeOfalready said the
right thing, "Neither means the token's default project", three lines above
the code doing the opposite.The issue offered two coherent answers and the cloud settled which one it
takes. Measured on fr-par, 2026-09-02, with a second project made for the
occasion and deleted after:request the other project's volume GET /volumes(no filter)visible GET /volumes?project=<the default>hidden GET /volumes?organization=<the org>visible So the create is authoritative: naming a project makes it real, and an
unfiltered list answers the account. The isolation the old behaviour was
defending is the filtered read, and it is untouched — a list naming a
project still answers that project alone.The acceptance entry on
instance/v1/API.ListServersis deleted, and the gate
asked for it in those words: "excused nothing this run: either it is fixed and
the entry goes, or the corpus no longer carries it". -
The cloud retired
b_ssdat creation and this emulator was still minting
it (#393). Every route that made a volume wrote that type:POST /volumes
defaulted to it when the request named none, a server'sroot_volumewas
forced to it whatever local type was asked for, an update that added a disk
defaulted to it, the catalogue images advertised it, and a snapshot with no
readable type fell back to it.fr-parrefuses it on both creation routes.Measured against a real account on 2026-09-02, and the two routes do not share
a wording — which is why both are transcribed rather than one being reused:request fr-paranswersPOST /volumesnovolume_type400 required— required key not providedPOST /volumesb_ssd400 constraint— b_ssd volumes are no longer supported. Use Scaleway Block Storage (SBS) volumes instead…POST /volumessbs_volume400 constraint— not a valid value (it isblock/v1's product)POST /volumesl_ssd,scratch201 POST /serversvolumes.0.volume_type: b_ssd400 constraintonvolumes.0.volume_type— Create volumes with volume_type=sbs_volume insteadPOST /serversl_ssd201, and the volume comes back l_ssd— honoured, not replacedPOST /serversscratch400 constraintonimage— Cannot use an image with a scratch volumeThis is the drift the surface scan cannot see: no Go signature changed, on
either operation. Only a recording of the cloud names it, which is the whole
argument for keeping one. The four acceptance entries that carried the
divergence incorpus/accepted.jsonare deleted, andfeint corpus --check
compares those exchanges for real now.A limit that moved with it: the local types are no longer overridden. A
create namingl_ssdgets anl_ssd, where it used to get ab_ssdunder a
comment explaining why. The reason that override existed is unchanged and
still holds the catalogue in place —volumes_constraint.min_sizestays at 0
so the CLI's sum of local volumes cannot refuse the creation it just asked
for.docs/limits.mdcarries the corrected paragraph.And one number moved with the type. The catalogue image's root volume is
l_ssdnow (measured: aDEV1-Screated onfr-parwithimage=ubuntu_jammy
answersimage.root_volume.volume_type: l_ssd,size: 10000000000), which
puts it inside the arithmeticscwruns over local disks. At the 20 GB this
emulator advertised, everySTARDUST1-Screate was refused with "image …
requires 20 GB on root volume, but root volume is constrained between 0 B and
10 GB".rootVolumeSizeis 10 GB, the same as the cloud's, and the image
endpoint reads that constant instead of repeating the number. The conformance
suite caught this and no unit test could have: the arithmetic lives in the
client.What it also settles: the decline on
instance/v1/API.ListVolumesTypes
rested on a sentence that has expired — this emulator makes neither of the
two types/products/volumeslists. It makes exactly those two now. The
decline stands on its other condition alone, and says so: no recording of that
route exists incorpus/, and it answers a table of per-type constraints that
rule 4 forbids inventing. -
A boolean query filter read one spelling and silently emptied the listing for
the others (#630).attached=truefiltered correctly;attached=True— what
the official Python SDK sends, becauserequestsrenders a Python bool that
way — matched nothing and answered a well-shaped empty page. So
IpamV1API.list_i_ps_all(attached=True), the documented way to ask that
question from Python, returned an empty inventory against this emulator and the
attached addresses against the real API.Measured on fr-par, 2026-09-01: the real API accepts
true,True,1and
the rest of whatstrconv.ParseBooltakes, because it is a Go service using
that function. This pack now parses with the same one.And an unparseable value is refused rather than swallowed. It used to be
read as false, which says the fleet is empty — a different and wrong statement
from the question was malformed. The two error dialects are the cloud's own
and are not one:instance/v1answers its typedinvalid_arguments, while
ipam/v1,vpc/v2,vpc-gw/v2,lb/v1,block/v1andiam/v1alpha1answer
strconv's error verbatim. Copying one onto the other would have been inventing
a format.The fix covers every boolean the pack reads from a query, not only the one
reported: eleven filters and, more seriously, two action flags. A client
sendingrelease_ip=Trueordelete_ip=Truekept its address and was told
nothing. Those two are also parsed before the object is looked up, which is
the order fr-par answers in — a 400 sends a client to its own request, a 404
sends it hunting for a resource.Reported by a consumer building a dynamic Ansible inventory for Scaleway.
Added
-
instance/v1/API.UpdatePrivateNICis served, and the refusal it replaces is
named here because a withdrawn refusal is the change a consumer builds around
(#624).PATCH /instance/v1/zones/{zone}/servers/{server_id}/private_nics/{id}
writes the tags a NIC carries; a request naming none changes nothing, which is
what makes a read-compare-write module idempotent.The decline had said the pack "stores no tag on a private NIC, so it would
answer success over a field nothing reads back". Both halves had stopped being
true:createPrivateNIChas stored the tags all along,listPrivateNICs
filters on them, and the read carries them on all three doors. A decline is a
decision and may be revisited; a decline's reason is a claim about the code,
and this one described a past state.It also makes an assertion real that the code had been waiting for.
modification_dateis served from the resource's own updated time, and the
comment beside it said no route modified a NIC, so the two dates carried one
instant and a test requiring them to differ could not have failed. This route
is what makes them differ.Reported by a consumer generating an Ansible Day-2 collection from the Scaleway
OpenAPI documents. -
instance/v1/API.GetDashboardandinstance/v1/API.GetServerCompatibleTypesare
served, and the refusals they replace are named here because a withdrawn
refusal is the change a consumer builds around (#626).Both had been declined under "capacity and quotas are the provider's fleet,
and a local emulator that answered would be inventing headroom a client could
plan against". A live read of fr-par-1 on 2026-09-01 withdrew that sentence
from both:compatible-typesanswers a plain list of type names with no
headroom in it, so it is a property of the catalogue rather than of the fleet;
and every key the dashboard returns names a family this pack serves, so the
"unemulated remainder" the decline invoked is empty. The two volume-type
counters this emulator never fills are zero because it makes none, which is
true of it rather than short of anything.GetServerTypesAvailabilitykeeps the sentence, and keeps it rightly: fr-par-1
answeredshortagefor every type, and any value served here would be a claim
about Scaleway's fleet.Neither has a
scwverb, so both declare why no client drives them rather than
pretending one does.
Changed
-
The upstream drift scan runs every night and reports through the
night-report mechanism (#705)..github/workflows/drift.ymlran on Monday
alone, and a scan that failed for a reason other than "the surface moved" (a
clone that would not fetch, a build that broke, a push that was refused) left
a job log and nothing else, the failure mode #501/#502 measured on the
runtime proof. The scan is on a nightly cron now, and anightjob adopts
night-report.yml: a night where the scan could not conclude opens one
scheduled-redissue, updated, closed by the first green night. The two
outcomes stay apart, and the tests hold that rather than a comment: the
surface moving is the pull request, as always, never a red night (the
Coverage reportstep'scontinue-on-errorkeeps the job green), and the
pull request stays conditional on a real diff, because a pull request every
night would teach everyone to close them unread. Measured on the five
scheduled runs since 2026-08-03: all green, none moved the surface, none went
red, so the two nights that matter are derived from the real payload of
2026-08-31 and say so. -
The Outscale catalogue's images are
ami-fe1a7001,ami-fe1a7002and
ami-fe1a7003, no longerami-00000001..3(#395); its seven net access
point services arepl-fe1a7001..7, no longerpl-00000001..7; and the
Exoscale default security group isfe1a7000-0000-4000-8000-000000000001,
no longer00000000-0000-4000-8000-000000000001. All three lived in the
namespace the
corpus sanitiser mints (feint transcript --sanitise: prefixed identifiers
as a counter in eight hexadecimal digits, UUIDs under
00000000-0000-4000-8000-), so a sanitised recording could name a fixture of
this emulator where it meant an object of the account: measured on
corpus/outscale/oapi-cli-refusals.jsonl, whereosc/Client.DeleteImage
andosc/Client.UpdateImageon an image the cloud refused with 400 replayed
asami-00000002, found the catalogue, and answered 409 "belongs to the
emulated catalogue". The two corpus exemptions carrying that are gone, and
feint corpus --checkcompares the exchanges. A test ininternal/cli
reads every string literal of every pack's source with the sanitiser's own
recogniser, so no pack can move a fixture back in. A client that hardcoded
ami-00000001changes one value; the README, the quickstart and every
conformance fixture did. -
instance/v1/API.ListVolumesTypesstays declined and its reason is replaced
(#625). The old one said a type list "would describe capabilities and
constraints nothing here can honour", which could not be what separates it from
ListServersTypes— that sibling is served and answers capabilities,
constraints and prices.What separates them is measured. The served catalogue names types this emulator
creates, and its values come from a recording of a real account. A live read of
/products/volumeson fr-par-1 answers exactly two types,l_ssdand
scratch, and this emulator makes exactly those two since #393 — the
sentence this decline first rested on has expired, and the entry for #393
above says so. What holds it now is its other condition alone: no
recording of/products/volumesexists incorpus/, and the route
answers a table of per-type constraints that rule 4 forbids inventing. -
instance/v1/API.ExportSnapshotstays declined (#627). Answering a
202-shaped acknowledgement without writing an object anywhere would be the
emulator saying something started that did not, which is the one thing it
exists not to do. The reason is infrastructural and measured in
docs/limits.md.
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)
| Exoscale, Outscale, Scaleway |
| OpenTofu | 1.12.5 | .github/workflows/conformance.yml (TOFU_VERSION)
| Exoscale, 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
| >= 0.71.0
| constraint: resolved fresh on each run, so the version that answered is not knowable here | yes |
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.
Every stack under examples/stacks/ is applied on every pull request.
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.