Skip to content

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

OpenHAL componentsFour clients, the MCP server, the CLI, the Python library and the dashboard, call the hub's local API. Inside the one hub process the API passes commands through tokens and principals to the safety kernel, which alone gives drivers a single-use grant and writes the audit log. The routine engine, watchdogs and confirmation queue also go through the kernel. Drivers reach the hardware and bridged systems.Clientsopenhal start, one processHardware andbridged systemsopenhal mcpstdio MCP serveropenhal CLIPython libraryopenhal.connect()Dashboardserved by the hubLocal APIFastAPI and WebSocket127.0.0.1:7420Tokens,principalsDeviceregistryConfirmationqueueDriverwriterRoutineengineWatchdogsSafety kerneldeterministic, no LLMDiscovery andlibrary matcherAudit loghash chainDriversprotocols and bridgessingle-use grant
ComponentModuleRole
Brandopenhal/_brand.pyEvery public name, per ADR 0002
Spec modelsopenhal/spec/Models for device files, routines and commands, units, the expression language, JSON Schema export
Safety kernelopenhal/kernel/Checks, grants, rate limit state, interlocks, watchdog budgets, stop lock, confirmations
Audit logopenhal/audit/Append-only SQLite table with a hash chain, per ADR 0010
Hubopenhal/hub/Daemon, registry, config, tokens, storage, local API, discovery, library matcher, dashboard files
Driversopenhal/drivers/Base class, registry, mock framework, one package per protocol or bridge
Routinesopenhal/routines/YAML and Python routines, triggers, scheduler
MCP serveropenhal/mcp/Tools and prompts that forward to the hub
CLIopenhal/cli/Typer application
Clientopenhal/client/Sync and async API client behind the CLI, MCP server and library
Driver writeropenhal/writer/Detection, sketch parsing, manual extraction, interview, staged test plans
Conformanceopenhal/conformance/The battery every driver must pass
Dashboarddashboard/Svelte 5 app, built into openhal/hub/static/
Websitewebsite/Astro site built from the repository
Firmwarefirmware/arduino/OpenHAL/Arduino library that gives a maker's device a self-description
Librarylibrary/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.

The life of a commandAn agent asks openhal mcp to pause the print. The hub API passes it to the safety kernel, which runs its checks in order. A blocked command is logged and answered with the reason. A command that needs a person is queued with an expiry. An allowed command gets a single-use grant, the driver verifies and consumes it, acts, and the kernel logs the result.Agent(MCP client)openhal mcpHub APISafetykernelConfirmationqueueDriverAudit logblockedneeds confirmationallowedaction pausePOST /v1/actionagent tokenevaluateunits, state,range, step,enumrate, runs,interlocks,watchdoghazard, scope,confirmationblock entryBlocked(reason)status blockedenqueuepending entryPending(id)issue a Grantaction withthe Grantverify andconsume itactresultaction entryDone(result)

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

Device discoveryopenhal discover looks for devices over USB, mDNS, VISA, BLE, ecosystem APIs and Raspberry Pi checks, identifies each with read-only commands, and asks the library matcher. A match offers a library file that loads read-only. A known protocol builds a draft file. An unknown device is listed and offered the driver writer.openhal discoverScansIdentify with read-onlycommands only*IDN?, firmware describe, M115Library matcherOffer the library file,high risk entries waitfor confirmed limitsdescribe() buildsa draft fileList as unknown,offer the driver writerUSB enumerationVID:PID, class,serial numbermDNS andZeroconf browseVISA resourcelistBLE scanEcosystem APIsMoonraker, OctoPrint,Home Assistant, ESPHomePlatform checksRaspberry Pi GPIOand cameramatchprotocol knownunknown

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 driver writerFor an unknown device the driver writer tries a library match, then a protocol driver's self-description, then a sketch, then a manual, then read-only probing. Each path ends in a draft device file with every limit marked suggested. A person answers the limits interview, approves a staged test plan, and the writer saves the device file, scaffolding a new driver first when one is needed.Unknown device1. Library match?2. Can a protocoldriver connect?4. Sketchavailable?5. Manualavailable?3. Self-descriptiondescribe(), firmware JSON,SCPI, GATT, OPC UAParse the sketchcommands, pins,ranges, unitsExtract commands,all unconfirmed6. Read-only probingknown read commandsDraft device fileevery limit suggested7. Limits interviewa person answers each limit8. Staged test plansmallest safe value first9. Needs new code?openhal driver newexperimental until tested10. Device file with provenanceand a library submissionyesnoyesnoyesnoyesnoyesno

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

Routine executionA trigger fires a routine. The routine engine checks it is enabled and approved, then sends each read and command to the safety kernel as the routine's principal. A blocked step stops the run. An allowed step goes to the driver with a grant. The engine records the run.TriggerRoutine engineSafety kernelDriverblockedallowedfireenabled? approvedfor high hazard?each read and commandas routine:<name>same checks asany clientBlocked(reason)stop the run,apply on_blockcommand witha Grantrecord the run

Routines run with no model as routine:<name>, with the scope of the human who enabled the routine, capped at control_medium.

Bridges

BridgesThe safety kernel sends a bridge driver a write with a grant. The bridge calls the bridged system, gets the new state and returns the result. The device file governs the limits even when the bridged system has its own.Safety kernelBridge driverBridged systemwrite(entity, value,Grant)service callor API requestnew stateresultThe device file governs limitseven when the bridged systemhas its own. Only entities aperson selects are exposed.

Stop

StopA stop from any client sets the kernel's global lock first, cancels pending confirmations and pauses every routine, applies each device's safe state, reports per device, and blocks commands until a person unlocks.stop_all from the CLI, MCP,dashboard, library or a GPIO buttonThe kernel sets theglobal lock firstCancel pending confirmations,pause every routineApply each device's safe statewith kernel-internal grantsReport per device: safe,failed or no safe stateCommands blocked untila person unlocksopenhal unlock or the dashboard,human token and typed phrase

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.json and the Pydantic models in openhal/spec/models.py.
  • The driver interface in openhal/drivers/base.py, with discover, identify, connect, describe, read, write, action, safe_state, health, close.
  • The kernel API in openhal/kernel/__init__.py, with Kernel.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 at docs/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.