OpenMHS

How it works

One gateway sits between everything that asks and everything that acts. This page walks through its parts, and follows one command from request to verified result.

Architecture

Diagram: AI agents over MCP, scripts over the Python SDK and people over the CLI all call the OpenMHS gateway. The gateway contains the local API, policy, the command engine, the journal and audit log, and the adapter host. Adapters for Modbus TCP, MQTT, SCPI, HTTP, OPC UA and ROS 2 connect it to machines.
Every interface calls the same local API, so an agent, a script and a person are held to the same rules.

The life of one command

An agent asks the simulated power supply for 2 V.

  1. Request. The agent calls submit_command with the device, the capability power.output.voltage.setpoint, the value {"value": 2, "unit": "V"} and an idempotency key.
  2. Journal. The gateway records the request before doing anything else (CREATED).
  3. Authorize. Policy looks for a grant that lets this principal write this capability on this device. No grant means DENIED.
  4. Validate. The value must have the right unit and be inside 0 to 12 V. The change from the current value must respect the slew limit of 2 V per second. The precondition "protection not tripped" must hold on a reading no older than one second.
  5. Approve, if needed. If the grant says require_approval, the command waits in AWAITING_APPROVAL until an operator decides. Preconditions are checked again after approval.
  6. Dispatch. The intent is journaled, then the adapter sends the write to the device (DISPATCHED, then ACCEPTED and APPLIED as the device reports).
  7. Verify. After a short settle time the gateway reads the setpoint back. Within tolerance (±0.005 V here) it becomes VERIFIED.
  8. Record. A receipt with the full history, the policy decision and the readback evidence is returned, and the audit log gains linked records.

Command states

Command state diagram: CREATED, VALIDATING, APPROVED, DISPATCHED, ACCEPTED, APPLIED, VERIFIED, with branches to DENIED, AWAITING_APPROVAL, TIMED_OUT, UNKNOWN_OUTCOME, RECONCILED_APPLIED, RECONCILED_NOT_APPLIED and VERIFICATION_FAILED.
The main states. The full table, including REJECTED, FAILED, EXPIRED, CANCELLED and UNVERIFIABLE, is in spec/lifecycle.md.

The rules that matter most:

  1. Acknowledgement is not effect. ACCEPTED and APPLIED are not success. Only a readback (VERIFIED), or a declared UNVERIFIABLE, is.
  2. Timeouts are uncertain, not failed. TIMED_OUT always moves to UNKNOWN_OUTCOME.
  3. No blind retries. A command in UNKNOWN_OUTCOME is never sent again automatically. Until it is settled, a new write or action to the same capability is refused (command.outcome_unresolved), unless the capability is declared idempotent or is a stop.
  4. Idempotency. The same principal, device and idempotency key return the existing command instead of running it again. Reusing a key with a different request is an error.
  5. Every command gets a receipt, including the ones that were denied.

Agents usually only need the coarse outcome:

OutcomeStatesPhysical effect
succeededVERIFIED, RECONCILED_APPLIEDConfirmed by readback
unverifiedUNVERIFIABLEDevice says yes; not independently confirmed
failedREJECTED, FAILED, VERIFICATION_FAILED, RECONCILED_NOT_APPLIEDNot as requested
deniedDENIEDNone, never sent
not_executedEXPIRED, CANCELLEDNone, never sent
uncertainTIMED_OUT, UNKNOWN_OUTCOMEUnknown

Unknown outcomes and readback

Suppose the gateway asks the supply for 7 V and the reply never arrives. Two things could have happened: the command was lost, or it was applied and only the answer was lost. From the gateway's side these look the same.

So OpenMHS reports UNKNOWN_OUTCOME, and nothing else is sent for that command. When the device can be read again, the command is reconciled: the gateway reads the device and records RECONCILED_APPLIED (it did take effect) or RECONCILED_NOT_APPLIED (it did not). An adapter crash after dispatch is handled the same way. Both cases appear in the demo report.

An agent that retries anyway is refused before anything reaches the device: a second write to the same capability waits until the first is settled. If the device has no readback, an operator checks it and records the answer with a reason (openmhs commands resolve); agents cannot. On SCPI instruments, a reply that arrives after its timeout is discarded by a *IDN? resync, never read as the answer to the next request, and nothing new is sent while the instrument is still busy.

Principals, grants and approvals

Example: what an agent may do, set by the operator from the CLI.

openmhs grants add --id agent-read --principal 'agent:*' --device '*' --capability '*' --ops read --effect allow
openmhs grants add --id agent-psu  --principal 'agent:*' --device sim.psu-01 --capability '*' --ops write --effect allow
openmhs tokens create agent:claude --scope 'sim.*:*:read' --scope 'sim.psu-01:*:write' --ttl 7d

Device descriptors

A descriptor is the device's contract. It is validated against JSON Schemas with stable error codes. An excerpt from the simulated power supply:

{
  "name": "power.output.voltage.setpoint",
  "kind": "property",
  "operations": ["read", "write"],
  "value": { "type": "number", "quantity": "electric_potential", "unit": "V",
             "range": { "min": 0, "max": 12 } },
  "side_effect": "reversible",
  "idempotent": true,
  "slew": { "max_change": 2, "per_ms": 1000 },
  "rate_limit": { "max": 5, "per_ms": 1000 },
  "preconditions": [
    { "id": "protection-not-tripped", "capability": "protection.tripped",
      "op": "eq", "value": false, "max_age_ms": 1000 }
  ],
  "verification": { "mode": "readback", "tolerance": { "abs": 0.005 },
                    "settle_ms": 50, "timeout_ms": 2000 }
}

Units use UCUM codes and are never guessed: a bare number where a quantity is expected is refused. Readings carry a timestamp, a source and a quality (GOOD, UNCERTAIN or BAD).

Adapters, profiles and the registry

An adapter translates between the gateway's contract and a device's protocol.

AdapterWhat it doesEvidenceLevel reached
Modbus TCPRegisters and coils, from a JSON profileIntegration-tested (simulated peer)L3 (declares L2)
MQTTTopics, with verified TLSIntegration-tested (simulated peer)L3 (declares L2)
SCPI (TCP or serial)Bench instruments such as power suppliesIntegration-tested (simulated peer)L3 (declares L2)
HTTPDevices with a web API, with verified TLSIntegration-tested (simulated peer)L3 (declares L2)
OctoPrint (Python, experimental)Read-only 3D printer status over OctoPrint's HTTP APIIntegration-tested against OctoPrint's Virtual PrinterL1 (declares L1)
OPC UA (Python, experimental)Read-only, using asyncuaNot verified: checked at L1 in CI, no report on filenone on file
ROS 2 Jazzy (Python, experimental)Observation only, in a pinned containerNot verified: checked at L1 in CI, no report on filenone on file

"Integration-tested" means the adapter spoke its real protocol, over real sockets, to a simulated device. It does not mean any particular real device works. The full, generated table is the compatibility matrix.

Modbus, MQTT, SCPI and HTTP devices need no code: a profile (JSON) maps the device's registers, topics, commands or endpoints to capabilities. openmhs profile new mqtt --id my.device-01 writes a starter profile from the tested example of each protocol. Adapters written in other languages run as out-of-process child processes over JSON-RPC on stdio. The adapter host supervises them with heartbeats and bounded restarts, quarantines one that keeps failing, and marks in-flight commands UNKNOWN_OUTCOME if it dies. Adapter manifests declare permissions and can be signed with Ed25519.

The adapter registry

A git-based list of adapters, one JSON file each, in registry/. Each entry points at the conformance report that supports it, pinned by SHA-256, and the compatibility matrix is generated from the entries and reports. Categories describe evidence, not endorsement:

CategoryMeaning
officialMaintained in this repository, with a passing report
communityMaintained elsewhere, with a passing report and an https source
experimentalPartial, or the evidence is incomplete
unverifiedListed without evidence

Simulators and fault injection

The virtual lab has three devices: a temperature sensor, a power supply and a linear stage. Each can be made to misbehave on request:

FaultWhat the simulator does
link_downAll communication fails, reads included
no_responseCommands get no answer and are not applied
lost_ackCommands are applied, but the answer never arrives
garbledCommands are applied, and the answer cannot be parsed
refuseThe device refuses commands and applies nothing

Conformance levels

The conformance suite tests an adapter together with its descriptor, sending real commands and injecting the faults above. Levels are cumulative.

LevelNameMeans
L0DiscoverableIdentity and capabilities can be inspected
L1ObservableState can be read with timestamps and quality; disconnects show as BAD, never as stale-but-good
L2ControllableAuthorized commands execute with verified outcomes; timeouts become UNKNOWN_OUTCOME; no retries of non-idempotent commands
L3CoordinatedTakes part in plans and leases; a stop gets through a lease

A level is reached only when every check at that level and below passed or did not apply. A check that could not run blocks the level. A conformance level is not a safety certification: it says the integration behaves as specified under the tested faults, not that a device is safe to operate.

Plans, leases and the stop lane

Where things are in the code

PathContents
spec/The specification and JSON Schemas. The source of truth.
crates/openmhs-typesDescriptors, values, units, lifecycle, validation with stable error codes
crates/openmhs-policyGrants, credentials, constraints
crates/openmhs-engineCommand engine, journal, approvals, leases, plans, twins
crates/openmhs-gateway, openmhs-apiGateway process and local JSON-RPC API
crates/openmhs-cliThe openmhs and openmhsd binaries, demo, scaffolding
crates/openmhs-mcpMCP server
crates/openmhs-hostOut-of-process adapter host, manifests, signatures
crates/openmhs-simVirtual lab with fault injection
crates/openmhs-conformanceConformance suite and reports
adapters/Modbus, MQTT, SCPI, HTTP, profiles, and Python adapters (OPC UA, ROS 2, OctoPrint)
registry/, conformance/reports/Adapter registry entries and the committed conformance reports behind the matrix
firmware/ESP32 test firmware used in Experiment 02 (run in QEMU)
python/Python client SDK
docs/adr/Architecture decision records: why each design was chosen

Next: get started.