What happened
A vendor station in conduit-go declared Awaits() naming one event type, and its Resolve body assumed that type. A different event arrived, was unmarshalled into the awaited stratum anyway, and the body read the resulting zero values as measurements. It filed an incident for a build that had just started.
Nothing malfunctioned. Every part behaved as written.
Three facts that combine into it
1. The station is told about everything. Owner ruling: the named controller gets every event on its run, because it walks the graph as events come in. So the common case for any station is an arrival it does not await.
2. Awaits() is declared and nothing enforces it. koine.Koine requires Awaits() []selector.Selector, and selector.Selector carries Type. The derived manifest records it too. But Guest.Deliver calls g.station.Decode(f) without ever comparing f.EventType against what the station declared. The declaration is generated, shipped, and read by no one at runtime.
3. Decoding cannot fail on a foreign event. Generated DecodeField switches on field NAMES and returns (false, nil) for anything unrecognised. Absent fields keep their zero values. So a foreign payload produces a valid-looking delivery with zeros, and a body cannot distinguish "absent" from "measured zero" after that point.
Why a plugin-side guard is not the fix
conduit-go has shipped one as a stopgap: its decode reads the station's own Awaits() and refuses a non-matching type. It works, and it is in the wrong place.
- Every station has this defect, so every author must remember to write the same check. One will not, and the failure is silent and plausible rather than loud.
Awaits() is already reachable from Guest.Deliver, immediately before Decode is called. The comparison belongs there, once.
The harder half: the wire has no word for it
Returning an error from Decode lands on wire.Refused, which abi.go documents as "the frame could not be read — a foreign version, a malformed projection, a station this guest does not serve. Nothing ran... Below the line."
An unawaited arrival is not below the line and nothing failed. It is the ordinary, majority case. Putting it in the fault register means the fault channel fills with normal operation, and a real fault becomes unfindable — the same defect as answering silence for an absent parent (#20), one layer over. Two different zeros wearing one value.
Done when
Guest.Deliver compares f.EventType against the station's Awaits() before calling Decode, so no station author writes this check.
- An unawaited arrival answers a determinate, ordinary outcome that is distinct from
Refused — nothing ran, nothing failed, and the host can tell the two apart.
ModeAbsent and ModeResolved selectors are considered: Type means an event type for ModeEvent/ModeAbsent and a subject for ModeResolved, so a naive string equality over all modes would be wrong.
- Whether this is a wire-contract change is settled explicitly.
wire.Version is 1; adding an Outcome value changes what a host may receive, and a host that does not know the new value must not misread it.
- A station declaring no awaited shapes is decided rather than defaulted: refuse everything, or receive everything.
Acceptance
- A guest whose station awaits only type A, handed a frame of type B, does not call
Decode, does not run the body, and answers the ordinary not-mine outcome — not Refused.
- The same guest handed type A behaves exactly as today.
- Mutation proof stated in the PR: remove the comparison and the first test goes red.
- No station in the conformance fixtures needs a guard of its own afterward, and conduit-go's plugin-side stopgap can be deleted.
What happened
A vendor station in conduit-go declared
Awaits()naming one event type, and itsResolvebody assumed that type. A different event arrived, was unmarshalled into the awaited stratum anyway, and the body read the resulting zero values as measurements. It filed an incident for a build that had just started.Nothing malfunctioned. Every part behaved as written.
Three facts that combine into it
1. The station is told about everything. Owner ruling: the named controller gets every event on its run, because it walks the graph as events come in. So the common case for any station is an arrival it does not await.
2.
Awaits()is declared and nothing enforces it.koine.KoinerequiresAwaits() []selector.Selector, andselector.SelectorcarriesType. The derived manifest records it too. ButGuest.Delivercallsg.station.Decode(f)without ever comparingf.EventTypeagainst what the station declared. The declaration is generated, shipped, and read by no one at runtime.3. Decoding cannot fail on a foreign event. Generated
DecodeFieldswitches on field NAMES and returns(false, nil)for anything unrecognised. Absent fields keep their zero values. So a foreign payload produces a valid-looking delivery with zeros, and a body cannot distinguish "absent" from "measured zero" after that point.Why a plugin-side guard is not the fix
conduit-go has shipped one as a stopgap: its
decodereads the station's ownAwaits()and refuses a non-matching type. It works, and it is in the wrong place.Awaits()is already reachable fromGuest.Deliver, immediately beforeDecodeis called. The comparison belongs there, once.The harder half: the wire has no word for it
Returning an error from
Decodelands onwire.Refused, whichabi.godocuments as "the frame could not be read — a foreign version, a malformed projection, a station this guest does not serve. Nothing ran... Below the line."An unawaited arrival is not below the line and nothing failed. It is the ordinary, majority case. Putting it in the fault register means the fault channel fills with normal operation, and a real fault becomes unfindable — the same defect as answering silence for an absent parent (#20), one layer over. Two different zeros wearing one value.
Done when
Guest.Delivercomparesf.EventTypeagainst the station'sAwaits()before callingDecode, so no station author writes this check.Refused— nothing ran, nothing failed, and the host can tell the two apart.ModeAbsentandModeResolvedselectors are considered:Typemeans an event type forModeEvent/ModeAbsentand a subject forModeResolved, so a naive string equality over all modes would be wrong.wire.Versionis 1; adding anOutcomevalue changes what a host may receive, and a host that does not know the new value must not misread it.Acceptance
Decode, does not run the body, and answers the ordinary not-mine outcome — notRefused.