fix(store, scaleway): what a pack stores is what a snapshot gives back, and the VPC body answers the name Scaleway publishes today (#567, #570) - #581
Merged
Conversation
…d it still holds (#505) `mise run limits:check` went red on the `lb acl delete` section because #505 closed on 2026-08-28, after the 2026-08-27 acknowledgement. A closed issue is not a lifted limit, and this one closed carrying `ships-as-a-limit`: it is documentation, not a fix. Read on 2026-08-28, and the two facts that would lift it are both still false: - `scw version` on this station answers 2.56.3, the version the section names; - `interceptACL` in `scaleway-cli/v2@v2.56.3` still guards its pre-fetch with `argsI.(*lb.ZonedAPIDeleteCertificateRequest)` where the argument is a `*ZonedAPIDeleteACLRequest`, so `getACL` is nil and the success path dereferences `getACL.Frontend.LB.Tags`. Only that section's line is re-dated. `limits-acks.py refresh` re-dates all fifty, and the tool says of it "This says somebody looked. It is only true if somebody did" — fifty were not read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ee real packs were not immune (#567) The maintainer's decision of 2026-08-27 settles where the model pack's half goes: `testdata/provider-four` is a pack to copy, so it stores the JSON shape like a real pack, and the shared layer does not learn to rebuild a `[]Rule`. That half is here. The half that survives it is the rule, and the rule is now a control rather than a habit. ## What the model pack did, and now does Measured on 2026-08-27 through `store.Snapshot` then `store.Restore` into a fresh store: `[]Rule` came back `nil`, `[]string` came back `[]any`, `map[string]string` came back `map[string]any`, and every reader asserted the Go type. A restored node wore no barriers, joined no segments and had no address, while the API went on describing all three. It now writes through named doors - `rulesAttr`, `stringsAttr`, `addressesAttr` and their readers - and the Go types stay in its own signatures, where they are useful. `TestTheFourthPacksNodeKeepsWhatItWearsAcrossASnapshot` asserts the behaviour rather than the shape: it snapshots, restores into a fresh store, reboots on a revived pack, and reads what the runtime was actually asked for. ## The issue's premise was wrong, and the measurement says so The issue states the three real packs are immune because they already store `[]any` and `map[string]any`. `storetest.GoShapes`, run over Scaleway's own barrage on 2026-08-28, reported 82 resources - security groups, snapshots, volumes and a VPC - holding a `[]string` in `Attrs["tags"]`. Nothing was broken by them, and that is why they lasted: `hasEveryTag` and `tagsOf` each carry a hand-written type switch tolerating both shapes, one per file, which is #542's seven copies one storey down. Exoscale carried the same defect in a sharper form - two helpers for one job, `labelsToAttr` right and `labelsOrEmpty` wrong, so an instance stored the JSON shape and a pool, a block volume and a load balancer did not. So the three packs are fixed too: `orEmpty` answers `[]any`, `labelsOrEmpty` is gone rather than repaired (a second door is a door the next author picks by proximity), and Outscale's `SecurityGroupIds` go through a new `idsList`. ## The rule, in the two places somebody meets it Prose beside `Resource.Attrs`, where an author decides what to put in it: the six shapes a restore returns, the three measured losses, and why numbers are the one exception (`resource.Number` has an answer, a `[]Rule` has none `internal/core` may know, rule 5). `storetest.GoShapes`, beside `Sweep`, for the same reason `Sweep` lives there: the invariant is the core's, the traffic is the pack's. It is added to the controls `TestEveryPackRunsTheSharedBarrage` discovers from a pack's own test sources, so a fourth pack fails a control it never had to remember to write. The control is tested in both directions: it names the three measured shapes plus one hidden a hop inside a legitimate container, it stays silent on every numeric width and every JSON shape a pack legitimately writes, and its wording is checked against a real Snapshot/Restore rather than against a belief about `encoding/json`. Population, stated rather than implied: `GoShapes` judges what a barrage produced, exactly like `Sweep`, so a write on a path no barrage drives is outside it. The source survey that found the exoscale and outscale sites is what covered those. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…, and the recording that predates the rename says so where it is read (#570) Scaleway renamed the vpc/v2 Object Storage family on 2026-08-25. The query parameter already accepted both spellings; the response body still carried the old one, on both objects. ## Decision 1: the new name alone, and it is the document that says so `.upstream/scaleway-openapi/vpc-v2.yml`, the VPC v2 product's own published schema, fetched 2026-08-28: scaleway.vpc.v2.VPC.object_storage_private_access_enabled (:796) scaleway.vpc.v2.PrivateNetwork.has_object_storage_private_access (:552) and each schema's `x-properties-order` — the document's own exhaustive property list — carries the new name and not the old. The SDK agrees (`vpc_sdk.go:1024` and `:646`). Nothing upstream declares a deprecation alias, so answering both would invent a window no source declares, which is the one thing rule 4 forbids. A test now asserts the old spelling is gone rather than merely that the new one is present, and its falsification is the body that answers both. The issue names the VPC half only. The private network was renamed in the same move and this fixes both; `has_s3_integration` was live on four doors. ## Decision 2: the 2026-08-20 recording is historical for the name, and for nothing else It is evidence of what a real fr-par account answered five days before the rename, and re-recording needs a paid account this repository does not have. What it still settles is what a rename does not touch: the field is present on every answer, and its value is false on an account with nothing attached. Nothing else in this repository establishes either. What it no longer settles is the spelling. That split is written at the assertion (`vpc_measured_test.go`), beside the code (`vpc.go`, both views), and in the two artefacts that replay the recording: - `corpus/accepted.json` gains 20 entries, one per (file, operation, kind, path), each naming exactly the field it excuses on the operation it excuses it on. The staleness rule deletes them the day vpc/v2 is recorded again; - `DeclinedFields()` gains one entry, and one only. The offline shapes store holds a default VPC and no private network, so ListVPCs is the single one of the fourteen recorded vpc/v2 operations whose element shape that gate reaches, and a decline that excuses nothing fails. ## Decision 3: nothing else moved, measured rather than assumed `mise run contract:update` on 2026-08-28 produced an empty diff. The issue says the contract was never re-extracted; it was, for #372 on 2026-08-27, and it already carries both new names. The pack was the only thing lagging. ## The blind spot, named because it stays `mise run drift:check` is green and would be green for any rename: it compares operations, not field names. So would the two field gates, each for its own honest reason. The omission gate (#88) lists a field both declared upstream and observed in a recording, and the new name is declared but unrecorded, so it sits in the "unconfirmed" list rather than in "missing". The contract check refuses an undeclared field only where the document closes the schema, and Scaleway closes none — the extraction reports "0 closed" for all 632. Three silent gates, three different populations, and a rename falls between all of them. What caught this was somebody reading the SDK beside the pack. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…, and it works (#548) #548 asks for the enforcement half: a server created WITH its flexible IP keeps an unfiltered routed NIC beside the private one the group covers. The honesty half shipped with #573; this is the measurement the enforcement half was waiting on. ## It still reproduces Under `--vm incus-ovn`, on 2026-08-28, from the API alone: a group whose inbound default is drop with one rule for 443, a server created with `public_ip`, its private NIC attached afterwards. eth0: nictype=routed, ipv4.address=203.0.113.2, no security.acls eth1: network=fnt-0da7a7bda1e, security.acls=scw-892dbc3d91e ## A third path, and it is neither of the two docs/limits.md already refuses Both refusals came from one place, the host route the routed NIC owns. The third takes the address off the device without taking the device off the instance, which Incus 7.2 accepts on a running instance: 1. `incus config device set <m> eth0 ipv4.address=` -> the host route is gone, and the device stays, so the profile eth0 on incusbr0 is never unmasked; 2. the uplink then takes the /32, where it answered `file exists` before; 3. `ipv4.routes.external` on the managed NIC, which is `routeAddressOVN`'s own gesture, and the guest repair that function already performs. ## The coverage is real, and the probe tells the two answers apart A listener sits on both ports, so a refusal cannot be an empty port: 203.0.113.2:443 connect_ex=0 OPEN a rule opens it 203.0.113.2:80 connect_ex=111 refused a listener is there, no rule is 203.0.113.2:8080 connect_ex=111 refused no listener: the negative control Before the migration, 80 on that address was OPEN. The machine keeps its way out: ipv4.nat=true on the OVN network, and ping 1.1.1.1 answers from inside with the station as the control. ## Why the driver is not changed in the same breath Two measurements, not reluctance, and both are in the section: - what a machine "answers on" moves. Incus.Inspect reports the first global IPv4 of the lowest-named interface, and after the migration both addresses share eth1: the runtime answers {"eth0":[],"eth1":["10.199.0.2", "203.0.113.2"]}, stable across three reads, so Binding.Address would report the private address where it used to report the public one. Scaleway is unaffected, publishing the flexible IP from its own store, and it answered public_ip: 203.0.113.2 throughout, but Binding.Address is the shared layer and the Exoscale pack reads it; - only OVN was measured. The bridge mode routes a public address through ipv4.routes on the device instead, and nothing here ran under `--vm incus`. So the bound in the table stands and functional.sh goes on skipping the public half. What changed is that "a sequence exists on paper" is now a sequence with numbers, and #548 has a remedy to implement rather than a question to answer. No code changed: the station was swept after the probe, and the emulator unwound its own uplink route on terminate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ved to, and the shape control says what it cannot see (#567, #295) Replaying `tools/falsify/specs/provider-four.json` after the Attrs change reported one mutation STILL GREEN, and the harness was right: the fourth pack writes a segment address into the map the store still shares, outside the lock and outside the conditional write-back (#295, #517) The property did not stop holding, it moved. The mutation removed the copy inside `withAddress`, and that copy is no longer what holds it: since #567, `addressesOf` decodes a stored `map[string]any` into a fresh `map[string]string`, so the conversion IS the copy and nothing `withAddress` does can reach the store's map any more. So the mutation is planted where aliasing is still reachable — `JoinSegment`, writing straight into `res.Attrs["addresses"]`, which `resource.Clone` shares with the store — and its label carries the reason it moved, so the next reader does not rediscover a green that means "the subject left". `TestTheFourthPacksNestedAttributesAreNeverWrittenThroughTheStore` is red under it again, and all thirteen of that spec's mutations bite. Also: `storetest.GoShapes` now states what it cannot see, where the next reader meets it. It judges what a barrage produced, exactly like `Sweep`, and that blind spot has already been paid once — its first run named 82 Scaleway resources and said nothing about the Exoscale pools, block volumes and load balancers storing a `map[string]string` on paths no barrage drives. Those were found by reading the packs' sources. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ince it accepts both (#570) The line named only `s3_integration_enabled`, which Scaleway retired on 2026-08-25. `listquery.go` narrows on either spelling, deliberately, so a client that has not been rebuilt is still served; naming one of the two made the sentence half true the day the rename landed. It now names both, and says why the *body* is the other decision: a request tolerates a name a client still sends, an answer may not invent a deprecation alias no upstream source declares. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #567. Closes #570.
#548 stays open, with its third path measured, working, and deliberately not
shipped — the last section says why, and it is the part of this branch worth
reading first.
#567 — the issue's premise was wrong, and the measurement says so
The model pack now stores the JSON shape through named doors, and
TestTheFourthPacksNodeKeepsWhatItWearsAcrossASnapshotasserts behaviour:snapshot, restore into a fresh store, reboot on a revived pack, and read what the
runtime was actually asked for.
The issue says the three real packs are immune. They were not.
storetest.GoShapes, run over Scaleway's own barrage, reported 82 resourcesholding a
[]stringinAttrs["tags"].Nothing broke, which is exactly why it lasted:
hasEveryTagandtagsOfeachcarry a hand-written type switch tolerating both shapes — #542's seven copies
of one tolerance, one storey down. Exoscale had the sharper form: two helpers for
one job,
labelsToAttrright andlabelsOrEmptywrong. All three packs arefixed —
orEmptyanswers[]any,labelsOrEmptyis deleted rather thanrepaired, Outscale gains
idsList.The rule now lives in two places, because prose alone is what let this last:
Resource.Attrs— the six shapes a restore returns, the threemeasured losses, and why numbers are the one exception;
storetest.GoShapes, registered in the shared barrage and wired into allthree packs'
barrage_test.go, among the controlsTestEveryPackRunsTheSharedBarragediscovers from a pack's own test sources —so a fourth pack fails a control it never had to write.
The maintainer's decision is respected exactly: the shared layer gained a
control, not a repair. Nothing in
internal/corelearned to rebuild a[]Rule, which it must not (rule 5).#570 — the new name alone, and three gates that were all honestly silent
What the body answers: the new name, and only it.
.upstream/scaleway-openapi/vpc-v2.yml— the product's published schema, fetched2026-08-28 — carries
object_storage_private_access_enabledandhas_object_storage_private_accessin each schema'sx-properties-order, thedocument's own exhaustive property list, and not the old names. The SDK
agrees (
vpc_sdk.go:1024,:646). No deprecation alias is declared anywhere,so answering both would have invented one. A test asserts the old spelling is
gone, and its falsification is a body that answers both.
The issue names the VPC half only; the private network was renamed in the same
move and is fixed too.
What the 2026-08-20 recording now means: historical for the spelling and
for nothing else. It still settles that the field is present on every answer
and false on an empty account. That split is written at the assertion, in
corpus/accepted.json(20 entries keyed(file, operation, kind, path), so eachnames exactly the field it excuses), and in one
DeclinedFields()entry —one, because the offline shapes store holds a default VPC and no private network,
so
ListVPCsis the only recorded operation that gate reaches, and a declinethat excuses nothing fails.
What else moved: nothing.
mise run contract:updateproduced an emptydiff. The issue says the contract was never re-extracted; it was, for #372, and
already carried both new names. Only the pack lagged.
The blind spot, named rather than implied
A rename passes between three gates, and each is honest in its own population:
drift:checkcompares operations, not field names — green for any rename;declared but unrecorded, so it has nothing to miss;
Scaleway closes none: 0 of 632.
Three silent gates, three populations, a rename between all of them. Recorded
here because nothing in this branch closes it.
#548 — a third path, measured, working, and not shipped
The defect still reproduces from the API alone under
--vm incus-ovn: a servercreated with
public_ip, private NIC attached after, ends witheth0 nictype=routed ipv4.address=203.0.113.2and nosecurity.acls, besideeth1on the managed network carrying its group.The premise was re-measured first: Incus 7.2 refuses
security.aclson a routedNIC —
Invalid device option "security.acls". So #337 holds and the remedy hasto move the address.
Both refusals already in
docs/limits.mdcome from one place — the host route therouted NIC owns. The third path takes the address off the device without taking
the device off the instance, which is neither of them, and Incus 7.2 accepts it
on a running instance:
ipv4.address=empty, then the/32intofeint-uplink'sipv4.routes— accepted, where it answeredfile existsbefore — then
ipv4.routes.externalon the managed NIC.Coverage, with a listener on both ports so that a refusal cannot be an empty
port, and a third port with no listener as the negative control:
Before the migration, port 80 on that address was OPEN. Outbound survives
(
ipv4.nat=true,ping 1.1.1.1answers from inside, the station as control).It is not shipped, and both reasons are measurements written into the section:
Incus.Inspecttakes the firstglobal IPv4 of the lowest-named interface. After the migration both addresses
share
eth1({"eth0":[],"eth1":["10.199.0.2","203.0.113.2"]}, stable overthree reads), so
Binding.Address/Started.Addresswould report the privateaddress where they reported the public one. Scaleway is unaffected — its API
answered
public_ipfrom its own store throughout — butBinding.Addressisthe shared layer, and the Exoscale pack reads it.
ipv4.routeson thedevice instead, and nothing ran under
--vm incus.So the bound stands,
functional.shgoes on skipping the public half, anddocs/limits.mdnow carries a sequence with numbers instead of "a sequenceexists on paper". Shipping it would have moved the problem from the firewall to
the address the API publishes, which is not a fix.
Gates
mise run conformancewhole suite, one emulator,FEINT_FIELD_GATE=1— green,267 s, 352/375, no omission failure ·
FEINT_VM=incus-ovn conformance:leg -- runtime— green, 639 s, doorstep clean both ends ·conformance:environmentgreen ·
prepush,docs:check,limits:check,falsify:lint(890 mutations,144 specs) green.
Falsification replays on the five specs whose subject this diff touches —
attrs-json-shape(new, 9),private-network-ipv6(8),corpus-findings(10),stored-numbers(10),provider-four(13) — every mutation bites.Two mutations did not bite at first, and in both cases the comment was wrong,
not the harness. One was planted in a loop the barrage never enters, retargeted
into
orEmpty's body. The other is #295's aliasing mutation, whose property hadmoved:
addressesOfnow decodesmap[string]anyinto a fresh map, so theconversion is the copy. Replanted at
JoinSegment, where aliasing is stillreachable, with the reason in its label.
Where
testplanwas wrongIts note "a change to the stored shape is proved across a restart by
mise run conformance:environment" is false:environment/up.shcontainszero occurrences of
snapshotor--state— it brings an emulator up and downand never saves or loads state. What proves #567 across that door is the unit
round trip, not that leg.
It also earned the OVN runtime leg because
provider-four/intents.goimportsinternal/core/machine, though this diff changes no driver code. Correct by itsrule, expensive in practice; it was run because #548 asked for it.
What was not obtained
accepted divergences stand until somebody records it, and the staleness rule
deletes them that day.
GoShapesjudges what a barrage drives. The Exoscale and Outscale siteswere found by reading sources, not by the control; that blind spot is stated in
its own doc.
testplanlisted were replayed — the five whose subjectthis diff touches were;
falsify:lintcovers the cheap half for all 144.