Architecture
One hub process owns every device and checks every command in one deterministic safety kernel. Thin clients, including the MCP server an agent connects to, send requests to the hub. The decisions are in docs/adr/, the safety rules in docs/SAFETY.md and the device file format in spec/SPEC.md.
Components
| Component | Module | Role |
|---|---|---|
| Brand | openhal/_brand.py | Every public name, per ADR 0002 |
| Spec models | openhal/spec/ | Models for device files, routines and commands, units, the expression language, JSON Schema export |
| Safety kernel | openhal/kernel/ | Checks, grants, rate limit state, interlocks, watchdog budgets, stop lock, confirmations |
| Audit log | openhal/audit/ | Append-only SQLite table with a hash chain, per ADR 0010 |
| Hub | openhal/hub/ | Daemon, registry, config, tokens, storage, local API, discovery, library matcher, dashboard files |
| Drivers | openhal/drivers/ | Base class, registry, mock framework, one package per protocol or bridge |
| Routines | openhal/routines/ | YAML and Python routines, triggers, scheduler |
| MCP server | openhal/mcp/ | Tools and prompts that forward to the hub |
| CLI | openhal/cli/ | Typer application |
| Client | openhal/client/ | Sync and async API client behind the CLI, MCP server and library |
| Driver writer | openhal/writer/ | Detection, sketch parsing, manual extraction, interview, staged test plans |
| Conformance | openhal/conformance/ | The battery every driver must pass |
| Dashboard | dashboard/ | Svelte 5 app, built into openhal/hub/static/ |
| Website | website/ | Astro site built from the repository |
| Firmware | firmware/arduino/OpenHAL/ | Arduino library that gives a maker's device a self-description |
| Library | library/ | Shared CC0 device files |
The life of a command
Every command takes this path, shown for an agent's pause of the printer. No other path reaches a driver's _write or _action.
A person approves a queued request with POST /v1/confirmations/{id}/approve and a human token. The kernel checks the request again against the current state, then follows the allowed branch.
Device discovery
Serial probing sends only identify commands documented as read-only for the protocol, with a short timeout, and never opens a port another process holds.
Driver writer
The user's own model takes part through MCP, per ADR 0006, and the hub runs every deterministic step. Nothing a model proposes can confirm a limit.
Routine execution
Routines run with no model as routine:<name>, with the scope of the human who enabled the routine, capped at control_medium.
Bridges
Stop
The lock comes before any safe-state step, so a racing command can't slip in. The kernel issues safe-state steps to itself, and those steps still pass the driver's grant check.
Contracts frozen after M1
spec/schema/device-file.schema.jsonand the Pydantic models inopenhal/spec/models.py.- The driver interface in
openhal/drivers/base.py, withdiscover,identify,connect,describe,read,write,action,safe_state,health,close. - The kernel API in
openhal/kernel/__init__.py, withKernel.evaluate,Kernel.execute,Kernel.dry_run,Kernel.stop_all,Kernel.unlock,Kernel.confirm,Kernel.reject. - The local API, versioned under
/v1, described by FastAPI's OpenAPI document checked in atdocs/api/openapi.json. - The MCP tool list and input schemas in
openhal/mcp/tools.py.
After the freeze, only the orchestrator changes these, and each change gets an ADR.