Architecture
Keystone is small enough to hold in your head. Three interfaces carry the entire design.
Where it sits
Before the internals, the outside view: who talks to a device, and what a device reaches for.
flowchart TB
FM["Fleet manager"]
BRK["MQTT broker"]
KS["keystone agent"]
ART["Artifact store"]
PROM["Prometheus"]
FM -- "plan, commands" --> KS
KS -- "state, health" --> FM
FM -. "or via a broker" .-> BRK
BRK <-. "commands, events" .-> KS
KS -- "download, verify" --> ART
PROM -- "scrape /metrics" --> KSNothing in that picture is a cluster. Devices do not know about each other, and the agent has no opinion about which device should run what — that decision belongs to the fleet manager.
flowchart TD
subgraph ADAPTERS["Adapters — how commands arrive"]
H["HTTP<br/><small>REST, :8080</small>"]
M["MQTT<br/><small>QoS, LWT</small>"]
end
CH["CommandHandler<br/><small>the business contract</small>"]
AG["Agent<br/><small>owns plan, components, truth</small>"]
SUP["Supervisor<br/><small>DAG, layers, readiness</small>"]
subgraph RUNNERS["Runners — how workloads execute"]
PR["ProcessRunner"]
CR["ContainerRunner"]
end
ART["Artifacts<br/><small>download, verify, cache</small>"]
ST["State<br/><small>runtime/state</small>"]
H --> CH
M --> CH
CH --> AG
AG --> SUP
AG --> ART
AG --> ST
SUP --> PR
SUP --> CRInterface 1: Adapter
A transport, nothing more. A Registry starts them all and stops them all with a
shutdown deadline. Adding a control plane means writing one of these — it cannot
accidentally introduce new behaviour, because all it can do is call the next
interface.
Interface 2: CommandHandler
Every adapter speaks this, and Agent is the only implementation. This is why HTTP
and MQTT cannot drift apart in behaviour: there is exactly one code path
behind them.
Interface 3: Runner
RunManaged is where a component actually lives: it starts the workload, probes
its health, applies the restart policy, and calls back on start, on each health
change, and on terminal exit. It returns only when the context is cancelled or the
component is terminally done — and when it returns, nothing is supervising that
component any more, which is a fact the reconcile logic depends on.
Two implementations: ProcessRunner (process groups and signals) and
ContainerRunner (containerd, with a CLI fallback).
Wiring
The whole startup, from cmd/keystone/main.go:
The pieces, and who calls whom
flowchart TB
subgraph AD["internal/adapter"]
direction TB
H["http"]
M["mqtt"]
end
REG["Registry"] --> AD
AD --> CH["CommandHandler"]
CH --> A["Agent"]The agent is the only implementation of CommandHandler, which is why the two
transports cannot drift apart in behaviour.
flowchart LR
A["Agent"] --> REC["plan_reconcile"]
A --> SUP["Supervisor"]
SUP --> G["Graph, TopoLayers"]
SUP --> FSM["Component FSM"]
FSM -. "hooks" .-> AThe dotted edge is the one worth noticing: the supervisor does not know what a component is. It calls hooks the agent supplied, which is why ordering logic and execution logic never tangle.
flowchart TB
A["Agent"] --> P["recipe, deploy"]
A --> ART["artifact"]
ART --> SEC["security"]
A --> STO["store"]
A --> STA["state"]
A --> MET["metrics"]Parsing, downloading, verifying, remembering and reporting — each in its own package, each called only by the agent.
The dotted edge is the one worth noticing: the supervisor does not know what a component is. It calls hooks the agent supplied, which is why ordering logic and execution logic never tangle.
Package map
| Package | Role |
|---|---|
internal/agent | Top-level runtime; implements CommandHandler; owns reconcile |
internal/adapter | Transport abstraction and lifecycle registry |
internal/adapter/{http,mqtt} | The two control planes |
internal/supervisor | Component FSM, dependency graph, layered start |
internal/runner | ProcessRunner, ContainerRunner, privilege dropping |
internal/recipe | Recipe parsing |
internal/deploy | Plan parsing |
internal/artifact | Download with resume/retry, SHA-256, signatures, cache GC |
internal/store | In-memory component and recipe stores |
internal/security | ECDSA/RSA detached signature verification |
internal/state | Snapshot persistence for crash recovery |
internal/metrics | Prometheus metrics |
internal/validate | Recipe/plan schema validation |
internal/runtime | Rlimits, PID liveness, cgroup placeholders |
On-disk layout
Everything is relative to the agent’s working directory: