OpenMHS Driver Profile
On this page 8 sections
OpenMHS is an independent open-source project, not affiliated with or endorsed by Anthropic, and not a reimplementation or compatible dialect of Anthropic's MHS research preview (appendix D). A driver package that passes the gate is evidence of a checked contract on a fake backend, never a safety certification.
| Field | Value |
|---|---|
| Document | driver-profile.md (normative) |
| Status | OpenMHS 0.9 development draft |
| Editor | The OpenMHS Authors |
| Layer | profile of the core: the deliverable of one upstream repository |
| Normative base | core.md (every rule there holds for a driver); the chapters 06 (adapters and bridges) and 01 |
| Reference implementation | mhs.driver, mhs.adapter, mhs.testing, mhs.bridge.* in MHS/sdk/src/mhs; the guide MHS/sdk/docs/driver-authoring.md |
| Data contracts | driver-lock.schema.json, driver-result.schema.json, check-report.schema.json, reference.schema.json, site-limits.schema.json, the task card schema of the registry |
A driver is the translation layer under the core (core.md CORE-002): it declares what a device reads, writes and does, with sourced limits and facts, and the runtime enforces everything else. The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are interpreted as in RFC 2119 and RFC 8174. DRV-nnn ids are defined here and only here.
1. The package
DRV-001: One upstream repository becomes one driver package, in one directory MHS/drivers/<track>/<owner>__<repo>/ (track is labs or robots), produced from one task card (MHS/registry/tasks/<track>/<owner>__<repo>.json) and merged in one pull request that touches only that directory. The layout is:
| File | Rule |
|---|---|
task.json | the task card; input, never edited by the author |
driver.py | exactly one mhs.driver.Driver subclass (decorated, or a config-only family subclass) |
fake.py and/or fixtures/ | the fake backend; fixtures/conformance.json supplies conformance inputs that synthesis cannot build |
native.py | the hardware backend factory; the gate never imports it |
facts.md | tags and facts, each with a source and path:LINE |
test_driver.py | calls mhs.testing.check_driver(HERE, backend="fake"); cases may be added and that call never removed |
driver.lock.json | the upstream pin, install, backends, authored_by, reviewed_by (DRV-040) |
reference.json, REFERENCE.md | generated by python3 -m mhs.driver reference <dir>; never hand-edited (CORE-030) |
site-limits.example.json | optional operator overlay example; it may only narrow (CORE-042) |
RESULT.json | pass, blocked or skip-proposed, with blocked codes; CI re-runs the gate and never trusts it |
DRV-002: The Driver class is the default deliverable. A declarative mhs-adapter.json with a bindings.json (06 §4) remains a valid, optional route for a bridge family whose natives are fully described by data; mhs.adapter.load serves both in strict mode with the same rules, and a package under MHS/drivers/ is a Driver package.
DRV-003: The upstream clone is read-only and lives outside the package tree (the card's local_path, under the private labs/ or robots/ inputs). The gate parses it with ast and never imports it; a package MUST NOT write into it, MUST NOT contain a symlink that leaves the package, and MUST NOT vendor or copy upstream code into the package. The driver's native is a "package.module:Class" symbol checked against the clone at the pinned commit (a GymnasiumDriver whose environment a module-level factory function of the clone builds from a class outside the clone names that function and declares native_factory, gym-live-routes.md). Upstream code runs only in a hardware backend factory (native.py) that imports it from the user's own installation.
2. The Driver API
DRV-010: A driver declares, with class attributes and decorators (INTERFACE.md lists the signatures, generated from the code): device_id, device_kind (from python3 -m mhs.driver catalog kinds), mode (hardware or simulated), risk (R0-R3, never below the card's risk_floor or a class floor), native, backends, tags, facts, safe_state (one sentence), estop_physical_ref, estop_latency_ms, estop_terminal (one sentence, only for a native stop that no reset can undo, CORE-087), watchdog_ms, vendor, model, display_name, resource_id (default main) and, for simulated drivers, fidelity. A decorated driver or a RosDriver MAY declare extra_resources (package composition: at most seven further typed resources of the same device, without parameters); an action then names its resource with resource= and is resolved against that resource's kind, while the device keeps one backend, one e-stop latch and one lifecycle.
| Decorator | Method | Served as |
|---|---|---|
@read(name, unit=, description=, type=) | def get(self) | a read-only state property (CORE-021) |
@write(name, unit=, limit=, description=, risk=) | def set_x(self, value) | a writable property, cap.device.configure.v1 (CORE-022) |
@action(cap="cap.<class>.v1", input={...}) | parameters named like the class fields | a catalogue class |
@action(name="x-cap.<owner>.<name>.v1", input=, output=) | every input needs a Field | an extension with an inline schema |
@estop | def stop(self) | the native stop; the runtime latches first, then calls it (CORE-074) |
@interlock(id, description=, blocks=[cap ids]) | returns True when closed | anything else, or an exception, reads unknown (CORE-073) |
@reset | optional native recovery that changes nothing on the device (re-establishing communication, SAFE-042); not allowed beside a reenable=True action (CORE-086) | runs after an accepted mhs.safety.reset (CORE-080) |
@action also takes long_running, max_duration_ms, cancellable, risk, examples, safe_state=True (a safe-state action takes no input and runs during an e-stop) and reenable=True (the one action that re-enables the device after a reset: it takes no input, is refused while latched and is never run by the reset; a driver that declares it has no @reset, CORE-086). Methods may be def or async def. A subclass that overrides an @estop method MUST decorate the override again. Family drivers (PythonObjectDriver, SerialDriver, ScpiDriver, GymnasiumDriver, MujocoDriver) are config-only subclasses: they set class attributes (parameters, reads, writes, commands, actions, safety_bindings) and no methods. Each commands entry of a family driver follows the canonical bridge grammar (06 §7, MHS/sdk/docs/bridges.md), so it declares its concurrency (shared, serialized or exclusive) and its timeout_ms.
DRV-011: A driver reports outcomes as core.md CORE-071 requires: an action returns a dict matching its output schema, a scalar (wrapped into the class's single output field) or None; a getter returns the value; a setter returns nothing or the applied value. A failure is DriverFailed(code, message, native_code=) with a code from the driver subset of the error registry (mhs.errors.DRIVER_SUBSET); cancellation is Cancelled; a refusal that actuated nothing is Busy; an effect that may have happened is OutcomeUnknown; any other exception is MHS.ADAPTER.SHIM_FAILED. A driver MUST NOT catch a failing native call and carry on, MUST NOT clamp, retry an actuation or decide that a refusal is fine, and MUST NOT convert a native return code into success: an SDK that reports failure as an integer needs a driver that raises on a non-zero code.
DRV-012: @estop may run while an action runs, on another thread. It MUST reach the real native stop through self.native, return promptly, and not wait for the running action or take a lock the action holds (CORE-072). A hardware or hybrid driver at R1 or above MUST declare @estop (or a safety_bindings stop) and a non-empty safe_state; at R2 or above it MUST also cite an independent physical stop (estop_physical_ref, path:LINE) or declare an @action(..., safe_state=True) (SAFE-045). A reset step may re-establish communication only when that changes nothing on the device; anything that may energise it, enable motion or start a process that may initialise hardware is never a reset step (SAFE-042). A driver whose stop leaves the device disabled binds the upstream's own re-enable as the reenable=True action and puts any native recovery the device needs after a stop, fault clearing included, in that action. A decorated driver that declares a re-enable MUST NOT define @reset (the SDK refuses it at definition and at start-up, and gate G3 fails), so its reset only releases the latch; a family driver's safety_bindings.reset MUST NOT call the re-enable's native target (a definition error) and MUST NOT re-arm the device through another target, which the SDK cannot detect and review checks (SAFE-042, CORE-086). A driver whose stop cannot be undone without a new native session (CORE-087) sets estop_terminal, and its @reset never reopens the session (SAFE-048). A simulated RosDriver whose only stop is a request that proves nothing by itself may declare safety_bindings["sensed_stop"] (SAFE-049, CORE-088): the e-stop sends the request and is achieved only on a bounded, fresh, producer-stamped confirmation; its thresholds are cited Limits or Limit.operator_required, each ROS 2 source declares its publisher's history (cited, or operator_required), its fake reads placeholders from fixtures/estop-confirmation.json, and no hardware driver may declare it; only the endpoint, binding the SDK's own route for a served manifest of mode simulated, lets its confirmation stand for a stop.
DRV-013: Backends are named hardware, fake and sim and nothing else. Only hardware drives a device. mhs.adapter.load and mhs.driver.build_server serve a driver in strict mode (CORE-160); a fake or simulator backend is served as mode simulated under <hardware id>/<backend> (CORE-111).
# runnable
from mhs.driver import Driver, DriverFailed, Fact, Field, Limit, action, build_server, close_server, estop, read, write
from mhs.testing import client_for
class FakeBath: # in a package: fake.py, only attributes the upstream class has
def __init__(self, **_options):
self.temp, self.setpoint, self.running = 21.0, 20.0, True
def get_temperature(self):
return self.temp
def set_setpoint(self, value):
self.setpoint = value
def start_ramp(self, target):
return -3 if target > 70 else 0 # the native SDK reports errors as return codes
def stop(self):
self.running = False
class Bath(Driver):
device_id, device_kind = "demo-bath", "thermal.zone"
mode, risk = "hardware", "R2"
native = "demo_sdk.bath:Bath" # the upstream class: parsed by the gate, never imported
backends = {"hardware": ".native:connect", "fake": FakeBath} # a package uses ".fake:FakeBath"
facts = [Fact("Setpoint range 5 to 80 Cel", source="vendor_doc", ref="MANUAL.md:9")]
safe_state = "heater off, circulation stopped"
estop_physical_ref = "MANUAL.md:15"
@read("temperature", unit="Cel", description="Bath temperature")
def temperature(self):
return self.native.get_temperature()
@write("setpoint", unit="Cel", description="Heater setpoint",
limit=Limit(min=5, max=80, source="vendor_doc", ref="MANUAL.md:9"))
def set_setpoint(self, value):
self.native.set_setpoint(value)
@action(cap="cap.thermal.setpoint.v1", description="Ramp to a target temperature",
input={"target_temperature": Field(description="Target temperature",
limit=Limit(min=5, max=80, source="vendor_doc", ref="MANUAL.md:9"))})
def ramp(self, target_temperature):
code = self.native.start_ramp(target_temperature)
if code != 0:
raise DriverFailed("MHS.DEVICE.FAULTED", f"start_ramp returned {code}", native_code=code)
return {"setpoint": target_temperature}
@estop
def stop(self):
self.native.stop()
server = build_server(Bath(backend="fake"))
client = client_for(server)
device_id = client.hello()["devices"][0]["device_id"] # mhs://localhost/demo-bath/fake
session = client.open_session(device_ids=[device_id])["session_id"]
def ramp(target):
return client.invoke("cap.thermal.setpoint.v1", {"target_temperature": target}, device_id=device_id,
session_id=session)
assert ramp(40)["status"] == "completed"
assert ramp(95)["error"]["mhs_code"] == "MHS.SAFETY.ENVELOPE_VIOLATION" # refused, no native call
assert ramp(75)["status"] == "failed" # the return code became DriverFailed
client.estop("demo", device_id=device_id, session_id=session)
assert ramp(40)["error"]["mhs_code"] == "MHS.SAFETY.ESTOP_ENGAGED" # latched until an operator reset
close_server(server)3. Limits, facts and provenance
DRV-020: A limit is declared once, on the field it bounds, with the grammar of CORE-040 and CORE-041: a bare min/max on an array bounds every element; min=[...], max=[...] with names=[...] and Field(length=N) bound each element; path="a.b", "a[*]" or "a[0]" reaches nested leaves. The author MUST NOT validate bounds in driver code: the runtime does (CORE-044).
DRV-021: Every Limit has a source (vendor_doc or upstream_code) and a ref (path/in/clone:LINE at the pinned commit), or is Limit.operator_required(reason). A bound that no source states MUST NOT be invented, and a catalogue class default is not a source for a particular device. An operator bound never appears in driver.py: it belongs in the operator's site-limits.json, which only narrows (CORE-042). Until the overlay supplies it, a call that sets an operator_required field is refused MHS.SAFETY.LIMITS_UNSET (CORE-046). A limit always cites the package's own clone. A task card MAY declare companion sources (companions: other corpus clones at their corpus-lock commits, from a reviewed registry override); then facts, a family driver's e-stop binding, its read-back, its declared re-enable and the bridges it declares that the operator must run (a ROS driver's operator_bridges) MAY cite companion:<alias>/<path>:<LINE> at that companion's pin, and nothing else may. One exception applies only to a package the user decided so for. A hardware driver whose family is direct or python-object MAY set companion_stop to exactly two members, one stop and one read-back, citing a fetched lock companion whose card entry sets hardware_stop true, on a profile that is not a ROS profile. Those two members need not exist on the package clone. The e-stop calls the stop member before the read-back. A limit, estop_physical_ref, action_space_ref and every other binding still cite the package clone. The citation does not waive reviewed_by.
DRV-022: A hardware or hybrid driver at R1 or above MUST limit every numeric input of every write and invoke it exposes, or mark it operator_required; a catalogue input it cannot bound is left out of the method signature. Otherwise the endpoint refuses the call at run time (CORE-046) and the gate fails G3.
DRV-030: A model-authored driver writes only vendor_doc and upstream_code facts and limits (CORE-032). watchdog_ms is declared only with a Fact that cites the device's own watchdog: the runtime implements no watchdog.
4. Provenance and human review
DRV-040: driver.lock.json records the upstream pin (repo_url, commit, license_spdx), the install steps, the backends, sdk_version, authored_by ({kind: "human"|"model", model_id, run_id, date}, model_id required for a model) and reviewed_by, which is null until a human signs {reviewer, date, scope: "contract"|"hardware", notes}. A model-authored driver of a hardware or hybrid device at R2 or above MUST NOT start its hardware backend while reviewed_by is null: the loader raises LoadError with reason review_required. A driver that sets companion_stop MUST NOT start its hardware backend while reviewed_by is null or its scope is not hardware, at every risk including R1. Any hardware backend additionally needs allow_hardware and a state_dir (CORE-154).
DRV-041: A human reviews every driver before it is merged, and before a hardware backend runs. A model never reviews its own work. The reviewer reads every Limit against the line it cites, because the gate only checks that the line exists; confirms that the @estop method calls the real native stop; checks the return-code handling the gate cannot see; reads native.py line by line (it is the only code that will run against a real device); and looks for aliased reflection the static scan does not follow. scope: "contract" means the code and limits were reviewed on fakes; scope: "hardware" is used only after a supervised run on the real device. Five percent of passing drivers are audited at random in each wave; a miss rate above ten percent means the prompt or the cards need work before the next wave (checklist: MHS/handoff/REVIEW.md).
DRV-042: Licence: a driver imports upstream code in its hardware backend and never vendors, copies or redistributes it. Running upstream code locally is allowed whatever its licence class, and neither the loader nor a bridge refuses a load because of a licence class. The licence class gates publication only: a task card with publish_review required (network-copyleft, source-available, proprietary or unknown licence) may still be implemented, and the owner decides before the package is published. This resolves ADPT-007 and ADPT-008.
5. Fakes
DRV-050: A fake is truthful. It implements only attributes the upstream native class has (gate G7 reads the clone's AST), except the two companion_stop members of a package the user decided so for when the card sets hardware_stop true; a serial or SCPI fake answers only command mnemonics that occur in the clone (IEEE 488.2 * commands excepted); it is not, derives from or re-exports the native class or the .native module (G3 FAKE_IS_NATIVE); it lives in the package unless the card's mock_strategy.preferred is upstream-sim (G3 FAKE_OUTSIDE_PACKAGE); each device gets its own fake object; and it makes the native stop observable. Preference order: the upstream's own simulator, a recording fake, a transcript. A fake that returns whatever a test expects proves nothing.
DRV-051: A pass on a fake or simulator backend is evidence state contract_checked and never an MHS-Device level (CORE-191). python3 -m mhs.driver check refuses --backend hardware; a hardware level comes only from an operator-run check on the real device under supervision.
6. The acceptance gate
DRV-060: A package is finished when python3 -m mhs.driver check <driver_dir> --backend fake exits 0 (1: a gate failed and the failing ids <gate>.<CODE> are printed; 2: usage error), or when RESULT.json records blocked or skip-proposed with codes. The command runs the gates in a child process with the network and device files disabled, never imports the package in the parent, cross-checks the child's report, and writes check-report.json (generated, not committed). CI re-runs the gate on every driver pull request and never trusts a committed RESULT.json or report. A model MUST NOT reorder, delete, skip or weaken gate checks, limits or tests to make the gate pass.
DRV-061: What each gate proves, and what it does not:
| Gate | Proves | Does not prove |
|---|---|---|
| G1 layout and isolation | required files exist; task.json names this directory and equals the registry card (a card that declares companions must have one); no symlink leaves the package; the clone exists, is unmodified and nothing wrote into it; each companion clone is at its pin and unmodified, and a fetched (lock) companion also matches its LOCK.json entry, holds no untracked file and no write bit | that the card is right, or that the code is any good |
| G2 lock | driver.lock.json validates and pins the card's package id, commit, repository and licence; authored_by is present | that the licence suits the use, or that a human reviewed anything |
| G3 strict resolve | catalogue classes and kinds resolve with no drift or widening; the card's expected mode and risk floor hold; limit paths resolve; hardware R1 and above has @estop, safe_state and limits on every numeric input; every Limit cites a source and a ref; a sensed-confirmation stop has a simulated card and a served manifest of mode simulated on the SDK's own ROS route, and fake placeholders for what is operator_required; the fake is not the native class; the package contains no forbidden constructs (exits, sys.argv, patching mhs, reaching into frames or sys.modules) | that a limit's number is true |
| G4 provenance | every ref path:LINE exists in the clone at the pinned commit, a path into a git submodule at the commit the pinned tree's gitlink records and only while the submodule is checked out at it (a cited line that does not show the number only warns) | that the line says what the limit says: an invented bound cited to a real line passes |
| G5 reference | reference.json and REFERENCE.md equal a fresh generation | that anything in them is correct |
| G6 behaviour | on fresh in-process endpoints: reads match their schema; in-range writes reach the fake with converted units; every bound, array element and nested leaf out of range is ENVELOPE_VIOLATION with zero native calls; an e-stop cancels a running action and reaches the native stop; actions are refused while latched; an agent reset is refused on hardware; a native failure is a failed receipt; operator_required is LIMITS_UNSET until the overlay fills it; outputs validate | that the fake behaves like the device; the gate injects exceptions, not return codes, so a driver that ignores an integer error code is invisible to it |
| G7 native surface | every attribute the fake defines or the driver touches exists on the native class in the clone's AST (for native_factory: every return of the factory builds the declared class, which lies outside the clone, and the fake stays within the environment interface of its profile); serial and SCPI mnemonics occur in the clone; ROS endpoints and types occur at their cited lines, a sensed source's fill_refs lines show their fields and its cited history line its QoS, and a companion-cited stop's read-back is a distinct call in the stop's namespace whose field is no acknowledgement and is assigned from a non-constant on its state_ref line. A direct or python-object driver that declares companion_stop exempts those two cited members from the package-surface rule when the card sets hardware_stop true, and the gate still requires the stop call before the read-back during the e-stop. The hardware backend still needs reviewed_by with scope hardware | that the upstream API does what the fake pretends, or that a factory's external class has that interface; that a cited QoS is the source's publisher's, or that a read-back's value is the state the simulator steps on |
| G8 protocol and surface | in-process conformance for profile MHS-Sim at CL3; the MCP projection builds; every input has a description and numbers a unit; each tool description is at most 2048 bytes | hardware conformance or any MHS-Device level |
| G9 repository tests | test_driver.py keeps the check_driver call and passes within 180 s with no network | that the cases it adds are meaningful |
DRV-062: The gate is a contract check on a fake, not a safety assessment. Residual risk: package code is evaluated in the gate's child process, and code written to defeat its checks is not ruled out mechanically. The static scan makes such code visible and the sandbox (DRV-070) bounds what it can do, which is why a human reads every driver (DRV-041).
7. The sandbox rule
DRV-070: The repositories a model reads are untrusted third-party code. A model run MUST be confined: it writes only inside its driver_dir, reads the clone, MHS/sdk, MHS/spec, MHS/schema, MHS/registry, MHS/handoff and other packages read-only, and touches nothing else (no home directory, credentials or other repositories). It has no network. The default mode is gate-only: the model reads the clone, writes the package and runs the gate, and nothing from the upstream is installed, imported or executed. A card whose mock_strategy.preferred is upstream-sim may run the upstream simulator in a private copy of the clone at the pinned commit (work/sandbox/<owner>__<repo>/src) with an empty virtual environment or container; the network is allowed only for the card's own install.commands, as a separate step before the model run, and the run itself has no network. A model never follows links in a README and never runs a command found there; a card with quarantine: true is never dispatched or run. PYTHONDONTWRITEBYTECODE=1 is set whenever a process reads a clone.
DRV-071: A model never edits MHS/sdk, MHS/spec, MHS/schema, MHS/registry, labs/ or robots/. An SDK defect is reported as RESULT.json status blocked with code SDK_BUG and a minimal reproduction in notes. The blocked codes are ENTRY_UNVERIFIED, INSTALL_FAILED, NO_HARDWARE_FREE_PATH, HELPER_MISSING, LICENSE_BLOCKED, NOT_A_DEVICE, QUARANTINE, PLATFORM_UNSUPPORTED, NEEDS_ARTIFACTS, CATALOGUE_GAP, SDK_BUG and GATE_TIMEOUT (defs.schema.json blockedCode).
8. Exemplars and the handoff kit
DRV-080: Four exemplar packages show the pattern and are the structural templates a task card's exemplar field points at:
| Package | Shape |
|---|---|
MHS/drivers/labs/tiagocoutinho__julabo | async direct Driver, R2 hardware, a circulating bath; a bound no source states is Limit.operator_required, narrowed by site-limits.example.json; native stop as @estop and as a safe-state action |
MHS/drivers/labs/thinksrs__srsinst.sr860 | ScpiDriver command map, R1 hardware, a lock-in amplifier; transcript-backed fake |
MHS/drivers/robots/xarm-developer__xarm-python-sdk | synchronous direct Driver, R2 hardware, a 7-axis arm; per-joint array limits, nested Cartesian limits, non-zero return codes raised as DriverFailed, emergency_stop as @estop, the recovery after an e-stop as the declared re-enable (reenable=True, no @reset hook), watchdog_ms backed by a Fact |
MHS/drivers/robots/farama-foundation__gymnasium | GymnasiumDriver family-config, simulated R0, CartPole-v1, complete fidelity contract |
All four are model-authored and unreviewed (reviewed_by null); their evidence is contract_checked. MHS/drivers/EXEMPLARS.txt lists them, and for d in $(cat MHS/drivers/EXEMPLARS.txt); do python3 -m mhs.driver check $d --backend fake || exit 1; done runs their gates.
DRV-081: The handoff kit in MHS/handoff/ (PROMPT.md, RECON-PROMPT.md, MASTER-PROMPT.md, PLAYBOOK.md, HARNESS.md, WAVES.md, SANDBOX.md, REVIEW.md, pilot-20.json and tools/) is the per-repository process this profile describes: the orchestrator picks a dispatchable card, prepare.py creates the driver directory with task.json, the model produces the package, collect.py re-runs the gate, a human reviews it, and one pull request per directory is merged after CI re-runs the gate. A card is dispatchable when its decision is direct-driver or family-config, its helper_status is available, blocked_on is empty, quarantine is false and its package_status is none or in_progress. The model's iteration limit is six gate runs; the time box is the card's timebox_min.