Skip to content

Architecture

OpenHAL is one hub process that owns every device, plus thin clients around the hub. Safety lives in one deterministic kernel inside the hub, so everything else, including the MCP server an agent talks to, sends requests to the hub.

The decisions behind this design are in docs/adr/, the safety rules are in docs/SAFETY.md and the device file format is 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.Clientsopenhald, 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 in one place (ADR 0002)
Spec modelsopenhal/spec/Pydantic models for device files, routines and commands, the unit registry, the expression language, JSON Schema export
Safety kernelopenhal/kernel/Every check, grant issuing and verification, rate limit state, interlocks, watchdog budgets, stop lock, confirmation decisions
Audit logopenhal/audit/Append-only SQLite table with a hash chain (ADR 0010)
Hubopenhal/hub/Daemon, registry, config, tokens, storage, local API, discovery, library matcher, static dashboard files
Driversopenhal/drivers/Base class, driver 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 client of the local API, used by the CLI, MCP server and library
Driver writeropenhal/writer/Detection, sketch parsing, manual extraction, interview, staged test plans
Conformanceopenhal/conformance/The standard 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 makes a maker's device describe itself
Librarylibrary/Shared device files (CC0)

The life of a command

Every command from every source takes this path, shown here for the printer's pause action from an agent, and no other path leads to 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 human approving a queued request sends POST /v1/confirmations/{id}/approve with a human token. The kernel evaluates the request again with current state (a confirmation never approves a stale check), 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,read-only until limitsare confirmeddescribe() 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 that are documented as read-only for the protocol being tried, at a short timeout, and never opens a port that 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 talk to it?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 participates through MCP (ADR 0006) while the hub does every deterministic step, and 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, and a routine's principal is routine:<name>, whose scope is 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 is set before any safe-state step, so a racing command can't slip in between. Safe-state steps are issued by the kernel to itself and still go through 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: discover, identify, connect, describe, read, write, action, safe_state, health, close.
  • The kernel API in openhal/kernel/__init__.py: 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.

Changes to any of these after the freeze are made only by the orchestrator and are recorded in an ADR.