docs: adopt read-only SQL-over-SSH as the peer query decision

Record the full reasoning path (GraphQL service, Arrow Flight, raw SSH,
forced-command synthesis) and specify read-only enforcement as five
fail-safe layers: key-per-operation, SELECT-only statement gate,
read-only OS user, engine hardening, resource caps. Promote 'SQL is
the contract' to a top-level design principle.
This commit is contained in:
2026-07-08 13:29:41 +01:00
parent 8f98a0c994
commit 523e15f490
3 changed files with 32 additions and 7 deletions
+30 -6
View File
@@ -85,14 +85,38 @@ Two deliberate non-features of the SSH channel:
- **No remote filesystem mounts in flight.** FUSE/sshfs over a lossy mesh inherits NFS hang semantics — processes block uninterruptibly when the link drops. Mounts are fine on the bench and at the dock; in flight, data moves by pull, never by mount.
- **No free-form remote execution.** Arbitrary drone-to-drone exec would mean one compromised unit owns the fleet. Instead, every permitted operation is an **SSH forced command** (`command="…"` in `authorized_keys`, one key pair per operation): the key *is* the API. Flexible like a CLI, auditable like an RPC, least-privilege by construction.
## Peer query standard
## Design decision: SQL-over-SSH as the peer query channel
For everything that is not broadcast, drones (and mission services) query each other explicitly. The contract:
**Decision: the in-flight peer query API is read-only DuckDB SQL executed through a dedicated SSH forced command.** This section records how the design got there, because the reasoning is as important as the result.
- **Schema-first:** all datasets share the versioned schemas from [03](03-data-platform.md); a query addresses `dataset + partition predicates + column projection + time range`.
- **In flight — query over forced command:** a peer query is a DuckDB statement (from an allow-listed template set) executed via its dedicated SSH forced command, streaming the result back as Parquet/Arrow over the same multiplexed session. No server process, no new port, no new protocol — and the receiving side lands data straight into its own store.
- **On the ground — GraphQL as the integration standard:** the warehouse exposes the same schemas through GraphQL for the multi-team surface, where flexibility and introspection matter more than grams and milliwatts. **Arrow Flight** is the upgrade path for heavy columnar serving from T3 if GraphQL result sizes become the bottleneck.
- Responses are always columnar batches (Parquet/Arrow), never JSON blobs.
### The path to the decision
1. **A GraphQL service on every drone** was the first candidate: one flexible, introspectable, schema-first contract. Rejected for the flight side — it means a resolver service, an HTTP stack, and a port on every flight-critical node, all to re-express what is already expressible in SQL against the local store. (GraphQL survives as the *ground* integration standard, where those costs are irrelevant.)
2. **Arrow Flight** — efficient columnar RPC, but again a server process and a protocol surface on board; its sweet spot is heavy serving from the ground warehouse, not grams-and-milliwatts peer queries.
3. **"Just give drones SSH to each other"** — the counter-idea: the transport already exists (ed25519, pinned known_hosts, one multiplexed session for bulk sync), so why not direct CLI access? Objection: free-form shell means one compromised drone owns the fleet; and remote mounts hang on lossy links. So — not raw SSH.
4. **The synthesis: SSH forced commands.** Keep the zero-extra-infrastructure transport, remove the general-purpose shell. Each `authorized_keys` entry binds a key to exactly one command; the query key runs exactly one thing: a read-only query wrapper. The peer's "API surface" is the set of keys it has provisioned — versioned, auditable, revocable per operation ([05](05-network-security.md), [11](11-cicd-delivery.md)).
What made the final option win: **the query language already lives on both ends.** Every drone runs DuckDB over the same versioned schemas; SQL *is* the contract. No IDL, no resolver layer, no serialization format to invent — the response is a Parquet/Arrow stream that the requesting side lands directly into its own store.
### Read-only, enforced in layers
SQL access must not become a write channel. A single "read-only connection" flag is not enough — DuckDB can materialize files via `COPY TO`, attach databases, or read outside the data root. Defense in depth, any single layer failing safe:
| Layer | Mechanism | What it stops |
| --- | --- | --- |
| 1. Key = operation | Forced command: the query key can only invoke the query wrapper, nothing else | Arbitrary exec, lateral movement |
| 2. Statement gate | Wrapper accepts a single statement, parses it, rejects anything but `SELECT` (no `COPY`, `ATTACH`, `INSTALL`, `SET`, multi-statements); parameters bound, not interpolated | SQL-as-a-write-channel, config tampering |
| 3. OS permissions | Wrapper runs as a dedicated user with **read-only filesystem access** to the data root and write access to nothing | Any write that slips past layer 2 |
| 4. Engine hardening | `:memory:` database, external access disabled except the data-root glob, extension loading off | Reaching outside the store |
| 5. Resource caps | Timeout, memory cap, niced CPU (flight software always wins), response size budget | Denial of service via expensive queries |
### The contract
- **Schema-first:** all datasets share the versioned schemas from [03](03-data-platform.md); a query addresses `dataset + partition predicates + column projection + time range`. Schema version is negotiated from the fleet release manifest — peers on one release speak one schema by construction.
- **Read-only SELECT over the local Parquet store**, streamed back as Parquet/Arrow batches over the multiplexed SSH session. Never JSON blobs.
- **On the ground — GraphQL as the integration standard:** the warehouse exposes the same schemas through GraphQL for the multi-team surface, where flexibility and introspection matter more than grams and milliwatts. **Arrow Flight** is the upgrade path for heavy columnar serving from T3 if result sizes outgrow it.
What this buys operationally: the dev/bench experience and the flight protocol are the same thing. An engineer debugging on the bench runs the identical query a peer drone would run — same wrapper, same permissions, same output format. There is no "debug API" that behaves differently from the real one.
## Event-driven end to end