In a review session an agent told me the plugins in the chain must not trust each other. Treat each one like a foreign API: check what arrives, and defend the chain against a plugin that misbehaves.
It was not a careless suggestion. That is what you do at a boundary you do not control, and it is the right default for most of the code anybody works on. I still said no, and writing down why took longer than saying it.
What a safeguard answers
Every check around a unit answers a question. Who owns this value now that I have it. What happens if the thing that handed it to me was halfway through. Where am I if this fails in the middle.
Those are real questions, and if the architecture leaves them open then something has to answer them. A safeguard is the cheapest place to put that answer.
In this chain they are answered already, so the safeguard would be a second answer to a settled question. What it builds is machinery: a check in every plugin, kept forever, guarding against something that was decided a level up.
So the check is not the problem. Wanting it is information. A check that compensates for a broken contract is mistrust, and mistrust is a symptom: where you reach for a safeguard, something above it did not decide.
What has to be decided
A boundary that holds. A unit is predictable from the outside — what it takes, what it returns, what may escape — and inside that it is free. That is Constraints Outside, Freedom Inside, and without it there is nothing to trust, because what a unit does to you is not knowable from where you stand.
One owner per piece of data. This is the one that fails quietly. Two components with responsibilities that sound unambiguous:
// Enricher owns the fleet model.
type Enricher struct {
model map[string]Instance
dns *DNS
}
// DNS owns the records it serves.
type DNS struct {
records map[string]Record
enricher *Enricher
}
Let them exchange data by reaching into each other:
func (e *Enricher) update(instance Instance) {
e.model[instance.Name] = instance
e.dns.records[instance.Name] = recordFor(instance)
}
Read the comments and the ownership is settled. Read the code and it is gone.
Each owns a name; both own the data. Now neither can trust its own fields, and
the safeguard the agent wanted is suddenly correct — DNS really does have to
defend records, because Enricher writes them.
A vocabulary between them. Don’t communicate by sharing memory, share memory by communicating, which is usually read as advice about concurrency and is also advice about this. Give the path a value:
type DNSUpdate struct {
Name string
Record Record
}
Enricher may produce an update. It does not apply one. DNS changes its own
records, and neither one knows the other’s machinery. That is
Every Plugin Has A Mailbox, and it is
what puts a field back on the list of things its owner may assume without
checking.
A failure mode somebody decided. Trust is not a bet that nothing breaks. It
is that what happens when something does was settled beforehand, in code rather
than in hope. The answer here is that the chain says when it does not know:
ChainState carries whether the picture is complete, /ready answers out of it,
and an instance running with no addresses is reported as exactly that instead of
as an opinion about what it means. Leaning on a component is possible because it
tells you where it stands, and that is the one thing a check downstream cannot
add.
A core that knows nothing. The fewer things the middle knows, the fewer places a decision can be made twice. That one has its own post.
Outside is the other way round
None of this travels across a process boundary. The Caddy sidecar I am planning holds no certificate: the process facing the internet has nothing worth taking, and the one holding the Incus connection listens on no public port. Full trust inside, none outside, and both on purpose.
The agent’s instinct was that one, applied a level too far in. It is the right discipline for a boundary you do not own, and an in-tree plugin is not one. It changes when I change it, in my commit, checked by the same compiler.
Where to look
Trust is not something extended to a component. It is what is left over when nothing needs defending, and it costs an architecture that decided the questions in advance.
Which makes the urge to add a check useful. It is a reading, and it points at the level above: something there is unowned, or unbounded, or fails in a way nobody named. The check will not supply any of those. It will only make the absence survivable, one call site at a time.
By René Jochum. License: CC-BY-4.0.
