Wiring the command pipeline: the ticker stops being a promise
July 9, 2026Deep DiveSecurityArchitectureZero-Trust
A few posts ago I drew an honest line: the command pipeline was designed but not actually enforced. The UI showed a status strip, but the send path went straight to the adapter. That strip was indicative, not proof. This post is the follow-through.
In the zero-trust honesty post I put the command pipeline in the "designed, not wired" column and said so plainly: the UI showed a pipeline status strip, but the actual send path was command → adapter. The strip was a promise, not a proof.
This post closes that gap. Every operator command now runs through an ordered chain of gates — RBAC → Safety → ROE → Authorization — before it reaches a vehicle. A veto anywhere short-circuits the rest and comes back as a rejection naming the gate that refused and why.
The interesting part isn't that it works. Ang interesting (what's interesting) is that wiring it was a small change, because the architecture had been holding a seam open for it the whole time.
Why this was the right thing to do first
Everything credible in this system hangs off this spine. Role-based access, the audit trail, per-command re-authorization — none of them mean anything if commands can route around the gates. Wiring the pipeline first turns "we have a security architecture" from a diagram into a property you can point at in the code and watch reject a command.
That's the difference between a story and a system, and it's the one I'd rather be able to demonstrate.
Next up is the recorder — an append-only, replayable log of every input and every decision — so that when a gate refuses a command, there's an immutable record that it did.
The pipeline design and implementation detail
Same password as the other GCS posts — or request access below.
What a pipeline stage actually is
The pattern underneath is Chain of Responsibility: an ordered list of stages, each of which can allow a command through or veto it. The core that runs them is a dozen lines — loop the stages, stop on the first veto:
for (const stage of stages) {
const result = await stage.evaluate(ctx)
if (!result.ok) return result // first veto wins; the rest don't run
ctx = result.value
}
return ok(ctx)
That's the whole engine. Everything else is stages, and stages are cheap to add because most of them are just a domain rule lifted into place.
Rules are data, not control flow
The safety and ROE gates aren't hand-written if ladders. They're Specifications — a small pattern where a boolean rule over a value is a first-class object that returns reasons on failure, not just false. A geofence, an altitude bound, a depth limit: each is a spec, and there's a helper that lifts any spec into a pipeline stage.
The payoff is that adding a rule never touches the pipeline. A new ROE constraint is a new spec dropped into the ROE gate; the engine, the other stages, and the send path don't know or care. That's the property that keeps a safety stack extensible instead of turning into a thousand-line conditional nobody wants to edit.
The two gates that need more than a pure rule — RBAC (which needs the operator's permissions) and Authorization (which needs a clock to check expiry) — take their inputs by injection. Which leads to the choice I'm happiest with.
One pipeline, many deployments — policy, not forks
Different deployments need different strictness. A simulator build should be permissive so I can develop against it. A real build needs tight roles and a live geofence. A training mode might disable engagement entirely.
The temptation is to fork the pipeline per mode. Instead, the pipeline is one thing, and the deployment injects policy: the RBAC rules, the geofence, the clock. Same code, different inputs. The permissive simulator and a locked-down real build run the identical pipeline; they differ only in what policy they hand it. Yun ang nagpapanatili nitong honest (that's what keeps it honest) — the security spine is shared, not copy-pasted and tweaked.
The gate I care about most
One stage is deliberately narrow: Authorization only does anything for an effector engagement — the highest-consequence command in the system. And it starts from a strong position, because of a property built in much earlier: you cannot construct an engagement command without an authorization token. That's a type-system constraint, not a runtime check. The stage's job is to verify that token at runtime — that it hasn't expired, and that it carries genuine two-person consent (two distinct approvers, not one person listed twice).
Belt, suspenders, and a second pair of eyes. The command can't exist without the token; the token is checked before anything acts on it; and a human — two humans — had to agree. None of that is novel on its own; stacking all three is the point.
Staying honest about what's still open
Here's the discipline holding: I'm not going to claim more than landed. Two things are still designed, not done:
The Sign stage — cryptographically signing each command — is next, and comes with the link-crypto work. Until then the chain is RBAC → Safety → ROE → Authorization, and I mark it exactly that way in the repo's status table.
The default RBAC in the simulator is permissive by design. Tightening the shipped defaults is a small, deliberate follow-up, not something to gloss as finished.
So the row that read "designed" now reads "enforced" — but only for what's actually enforced. The honest table is the worklist, and one line just moved.
Build log entry. Apache-2.0. Part of the multi-domain GCS build log.