Skip to content

A station is told about every event and has no ordinary way to say 'not mine' #21

Description

@sol-duara

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

  1. Guest.Deliver compares f.EventType against the station's Awaits() before calling Decode, so no station author writes this check.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions