feat(machine): a pack cannot name the runtime, and the compiler is what says so (#514) - #579
Merged
Merged
Conversation
…at says so (#514) machine.Binding's went unexported, and `p.binding().Driver.EnsureNetwork(…)` stopped compiling. It left the way to *name* one open. Measured on 154c204, in an isolated worktree: this file, dropped into internal/providers/scaleway/, compiled and `go build ./internal/providers/scaleway/` exited 0. package scaleway import "github.com/stephrobert/feint/internal/core/machine" var _ machine.Driver So the surface was held by TestNoPackReachesPastTheDeclaredDriverSurface alone — a convention plus an AST scan, which is what #514 §2.1 says is not enough, because a scan is a list somebody can widen and a build error is not. Driver, Router, Firewaller, Peerer, Isolator and Balancer are unexported now. That is stronger than the internal/ package rule the issue suggests and costs no new package: internal/ would still admit any future internal/core/machine/* sibling, and it would have dragged Spec, NetworkSpec, Attachment and the firewall and balancer vocabulary along with the interfaces or left a screen of aliases behind. What leaves the package instead is machine.Runtime, and it is a struct rather than a narrowed interface on purpose: an assertion needs no name, so rt.(interface{ Remove(context.Context, string) error }).Remove(ctx, victim) would have undone the whole thing in one line — and that shape is already in the repository twice, in machineDriver's Verify half and in `feint images remove`. A struct with an unexported field cannot be asserted on, so its method set is the whole of what a holder can do. It carries the operator's half — identity, capabilities, survey, prune, repair, watch, images — and no verb that moves a machine, a network, an address or a rule set. Those stay with Binding, Reconciler and GroupSync, where the ownership checks and the one order are. internal/cli is the caller that legitimately needed a name, and it needed thirteen in production plus some forty in tests. It holds machine.Runtime everywhere now, and five copies of `driver.(machine.Noop)` became one Runtime.Runs(): a question written five times is a question one caller answers differently. Every optional-half assertion it made — Surveyor, Pruner, Repairer, Watcher, UplinkReleaser, ImageBuilder, ImageLister and one anonymous RemoveImage — is a Runtime method keeping the three outcomes, so "nobody could be asked" never reads as "there is nothing". The proof is a test that compiles and requires the failure, not a comment claiming it. internal/cli/testdata/bypass holds nine packages: eight that must not build, one that must. TestThePacksCannotNameTheDriver builds each and checks the message names the right symbol; the ninth, admitted/, runs first and is fatal, because a probe failing for a wrong import or a module boundary reads exactly like the door being shut. tools/falsify/specs/driver-unnameable.json plants six mutations — the driver re-exported, the balancing half re-exported, the routing half re-exported past the ratchet, Env republishing its runtime, Binding republishing its driver, and the probe harness pointed at a directory that is not there. All six compile and all six bite. TestNoPackReachesPastTheDeclaredDriverSurface stays, and mustStayOutside gains a sibling that inverts: mustNotBeNameable asserts the six are *not* exported and still exist as unexported interfaces, so re-exporting one fails a named test instead of quietly putting the boundary back behind a scan. What this does not close, written down rather than implied: machine.Noop, machine.Incus and machine.Recorder stay exported — the metadata-only default, the only runtime, and #515's shared recorder are all needed by name outside the package — so `machine.Noop{}.Remove(ctx, n)` in a pack still compiles and is caught by the scan. The two controls are not alternatives. PackSurface and its exemption ledger are untouched, the ledger is still empty, machine.NewRecorder and its twenty-one gestures are untouched, and testdata/provider-four compiles and replays green through the contract alone. Four falsification specs named fragments this rename moved and were retargeted at the code that carries the same guard today; falsify:lint finds all 865 mutations again. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
#514) Two passages claimed what #514 measured to be false. docs/architecture.md: "a pack receives no machine.Driver value at all … so the call it would have written does not compile". docs/fourth-pack.md: "it could not name machine.Driver if it tried: since #511 emulator.Env hands out no driver value". Both were true of *obtaining* a driver and neither was true of *naming* one. On 154c204, `var _ machine.Driver` in internal/providers/scaleway compiled and `go build ./internal/providers/scaleway/` exited 0. That is the shape this repository calls a comment standing in for a control, and documentation is where it survives longest, because nothing runs it. They now say which step closed which half, and name the test that compiles the forbidden sentence. docs/limits.md loses two references to types that are unexported since: the driver's Detach and the runtime's balancing half. The passage on #475 keeps `machine.Firewaller` — it is explicitly "until #475 was fixed", and rewriting a dated record to match today's spelling would make it say something that was not what was measured. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rator types it The parameter was called alias while every caller passes the bare <family>/<version> the CLI reads, and the driver is what prepends the emulator's own prefix. A name that asserts a shape the value does not have is the smallest version of the defect this lot is about. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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 #514.
#514's §2.1 states criterion (a) as a specification: "
var d machine.Driver— orany driver-method expression — in a pack does not compile". Two of its three
closures had shipped (
Envno longer publishes the driver,Binding.Driverisno longer an exported field) and they removed every call site. The third had
not. Measured on
154c204, this file dropped intointernal/providers/scaleway/:go build ./internal/providers/scaleway/→ exit 0.The surface was therefore held by
TestNoPackReachesPastTheDeclaredDriverSurfacealone — convention plus an AST scan, which §2.1 itself says is never an API
that cannot be bypassed. The correction of record: the criterion was held one
layer below its promise, and nothing wrote that down. It was not unheld.
The proof, and it is a test that requires a compilation to fail
internal/cli/testdata/bypass/— nine packages, eight that must not build,one that must.
TestThePacksCannotNameTheDriverbuilds each and requires thefailure plus its expected message, so a probe failing for another reason is
not counted:
driver/(the file above, verbatim)undefined: machine.Driverrouter/ firewaller/ peerer/ isolator/ balancer/undefined: machine.<Half>envdriver/emulator.Env{}.Machines undefinedbindingdriver/b.Driver undefinedadmitted/Registered probes and on-disk directories are checked against each other both
ways. 0.66 s.
tools/falsify/specs/driver-unnameable.json: six mutations, allcompile, all bite — including the harness pointed at a directory that does not
exist, which is the control catching a probe suite that compiles nothing.
The form, and why it is not the one the brief proposed
machine.Runtime— a struct with an unexported driver field, built bymachine.Use(d). Not Go'sinternal/rule, and not a narrowed interface.A struct, because an assertion needs no name.
rt.(interface{ Remove(context.Context, string) error }).Remove(ctx, victim)compiles from anywhere, and that shape is already in this repository twice — so
it would have been copied. Visibility on a name is not visibility on a shape;
only an unexported field makes the two the same.
internal/core/machine/internal/xwould also still have admitted any future sibling of
internal/core/machine/*,and would have dragged
Attachment,FirewallSpecandBalancerSpec— all inPackSurface, all required to stay nameable — into the sub-package or behind ascreen of aliases.
Runtime's method set is the operator's half only:Name,Available,Runs,Capabilities,DeclaredCapabilities,Verify,Survey,Sweeps,Prune,ReleaseUplink,Traps,Repair,Watch,BuildsImages,LocalImages,RemovesImages,RemoveImage,BuildImage,Inventory,DerivedInventory.No
Start,Stop,Remove,Attach,Detach,EnsureNetwork,RemoveNetwork,RouteAddress,EnsureFirewall,PeerNetworks,EnsureBalancer.Two incidental wins: five copies of
driver.(machine.Noop)collapse into oneRuns(), and every optional-half assertion became a method that keeps the threeoutcomes — so "nobody could be asked" can no longer read as "there is
nothing".
The three acceptance criteria, judged one by one
(a) — closed by this branch, with a residue named rather than implied.
machine.Noop,machine.Incusandmachine.Recorderstay exported, so adriver-verb expression in a pack still compiles. Measured, not assumed:
machine.Noop{}.Remove(ctx, "feint-scw-x")builds, andTestNoPackReachesPastTheDeclaredDriverSurfacenames both reaches with pack,gesture and line. Closing that means unexporting
Incus(~1000 receiver sites)and taking
Noopfrom twelve test files and fromNewRecorder. Read literally,(a) is closed for the driver interface and its five pack-facing halves, and held
by the scan for the three implementations — and that sentence belongs in the
issue, which is why it is here.
(b) — was already closed by #517, and this branch did not weaken it.
testdata/provider-fourcompiles and replays green through the contract alone;provider-four.json's thirteen mutations all still bite; the pack stays indisciplinedPackDirsandTestTheDisciplineDetectorsReadTheFourthPackstillresolves its ≥40 reaches. Not one line of it was touched.
(c) — now an observation rather than a claim. This was the last thing
missing, and it was missing for a reason that had nothing to do with this branch:
conformance:functionalwas red onmainfor the isolation-detach race, fixedsince in #577 and #578. Replayed on this rebased tree, both stacks, zero FAIL:
The Outscale witness finally has a before and an after. Alongside it: the
fieldsleg byte-identical before and after (352/375, the same nine declinedfields with the same reasons, 316 operations compared, the same 398/149 blind
spot), both runtime legs green in both modes, and all prescribed falsifications
biting.
Gates
prepush0 after the rebase ·conformance:leg -- probe0.6 s ·conformance:leg -- fields209 s, 352/375 ·conformance:environment0 ·FEINT_VM=incus-ovn conformance:leg -- runtime614 s — isolation asserted,balancer dataplane 6/6 across both machines ·
FEINT_VM=incus conformance:leg -- runtime246 s — isolation skipped with its message(
capabilities.isolationfalse), everything else identical ·conformance:functional289 s, both stacks · 45 falsify specs prescribed bytestplan, 45 pass.The mode split is exactly what
machine.Capabilitiespromises: the bridgedeclines isolation and the balancer suite out loud, the rest holds in both.
What was not obtained
/_feint/healthhas one behaviour change on a path no production code takes: ahand-built
&emulator.Env{}with no runtime used to publishcapabilities: null; a zeromachine.Runtimenow publishes Noop's declaration (all false).DefaultEnvandserveboth set Noop explicitly, so nothing observable moved,and
TestAnUndeclaredDriverIsNotTheSameAsOneThatDeclaresNothingstill passes.(
a-run-ends-where-it-could-start,one-machine-per-address,run-leaves-nothing,trapped-station).falsify:lintcaught all six deadmutations in a millisecond; they are retargeted at the code carrying the same
guard today.
Where
testplanwas wrongIt did not ask for the bridge-mode runtime leg, and its own footnote says
"the leg runs under OVN; a bridge is a different verdict for isolation alone".
That sentence is false, and #574 was its counter-example the day before: under the
bridge the verdict differed for the firewall. The bridge run was played
because the brief mandated it, not because the plan asked. That line deserves its
own correction, which is not made here — fixing an instrument's prose without a
control is the pattern this milestone exists to remove.