OpenMHS Core
On this page 11 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). Nothing in this document is a safety certification.
| Field | Value |
|---|---|
| Document | core.md (normative) |
| Status | OpenMHS 0.9 development draft (spec_version 0.9, wire revision mhs_version 1.0) |
| Editor | The OpenMHS Authors |
| Layer | core |
| Precedence | schemas, then this document, then driver-profile.md, then chapters 01-10, then the appendices, then 00-architecture.md (informative); see README §1 |
| Reference implementation | MHS/sdk/src/mhs (Python 3.10+, standard library only) |
| Data contracts | the JSON Schemas in ../schema/, capabilities.json, errors.json |
| Optional profiles | profiles.md (sessions and permits, evidence, sim/fidelity/replay, events, host, registry, governance) |
This document is the short normative core: what an endpoint, a bridge or driver, and a client MUST do so that a model-driven command reaches a device only through the limits, interlocks, permits and e-stop of the endpoint. It states the rules as the 0.9 reference implementation realises them. The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, MAY and OPTIONAL are interpreted as in RFC 2119 and RFC 8174.
0. Conventions
CORE-001: A rule that cites an existing requirement id (for example SAFE-040) is defined in the cited chapter; this document says how the 0.9 reference implementation realises it and never weakens it. A CORE-nnn rule is defined here and only here. Where a chapter and this document disagree, this document wins (README §1).
CORE-002: "The core" is the endpoint runtime that owns safety enforcement (reference: mhs.server). "A bridge" or "a driver" is the translation layer under it (reference: mhs.bridge, mhs.driver). The core MUST enforce every rule of this document before any bridge I/O, and MUST NOT rely on the bridge, the driver or the model to enforce one.
CORE-003: OpenMHS enforces a software envelope. It is not a safety-rated channel and certifies no device, process or experiment: MHS.SAFETY.NOT_CERTIFIED is in hello.features (SAFE-050), reference files carry safety.not_certified: true (CORE-030), and every conformance claim, badge and document keeps the wording of SAFE-052 and SAFE-053. A physical e-stop independent of OpenMHS is the device owner's responsibility (SAFE-005, SAFE-045, SAFE-046).
1. Object model
| Object | Identifier | Defined by |
|---|---|---|
| Endpoint | endpoint_id, mhs://host[:port][/prefix] | 02 §1.5 |
| Device | device_id (a URI below the endpoint), instance_id (ULID) | device.schema.json |
| Resource | resource_id, a state document with a state_schema | device.schema.json |
| Capability class | cap.<class>.v<N>, or an extension id (x-cap.<owner>.<name>.v<N>, or cap.x.<vendor>.<class>.v<N> once published, CAP-035) | capabilities.json, 03 |
| Binding | the pair (capability_id, resource) with its schemas and limits | 03 §5 |
| Command | command_id (ULID), the idempotency key | CORE-090 |
| Receipt | receipt_id (ULID) | receipt.schema.json, 02 §10 |
| Operation | operation_id, op_ + ULID | 02 §7 |
| Session | session_id, opaque (mhs_s_ + ULID + random suffix) | 02 §5 |
| Principal | {id, kind}, kind is human, agent or service | CORE-150 |
| Limit | a closed rule keyed by a limit path | CORE-040 |
| Reference file | reference.json and REFERENCE.md | CORE-030 |
CORE-010: An endpoint serves devices. A device has one mode and one risk_class, resources, capability bindings, interlocks, and the safety declarations estop and safe_state. A binding is identified by (capability_id, resource); a pair MUST appear at most once per device, and the endpoint MUST refuse to start otherwise, in every mode. The runtime looks bindings up by that pair and never falls back to another binding's schema or limits. A class that is not in the catalogue is an extension (CAP-035) and carries an inline schema; the declared verb of an extension is not trusted wherever the catalogue verb matters (CORE-021, CORE-046, CORE-075).
CORE-011: Ids minted by the endpoint are opaque to clients. Session ids, subscription ids (sub_ + 128 random bits) and permit ids (permit_<ULID>_<64 random bits>) MUST NOT be guessable; operation_id is op_ + a full ULID. Clients MUST NOT derive meaning from their shape.
CORE-012: A device's risk_class (R0-R3) is never below the highest class floor of its bindings (SAFE-007) and is shown wherever a capability is listed (SAFE-010). A binding may raise a class risk, never lower it (SAFE-008).
2. The three primitives
CORE-020: Everything a client can do to a device is one of three primitives: read, write or action. Read is mhs.resource.read (and catalogue classes whose verb is read); write is mhs.resource.write; action is mhs.command on any other binding. Writes and actions pass the same evaluation order (CORE-060) and produce receipts; no other method changes device state, and an extension method (PROTO-003) MUST NOT bypass that order.
CORE-021: A read returns {revision, state, state_hash, sampled_at} for one resource. It never advances the revision (OP-008, PROTO-023), is not refused by the e-stop latch and needs no limit. Only a catalogued class whose catalogue verb is read counts as a read for the latch exemption, for LIMITS_UNSET (CORE-046) and for the revision rule; the declared verb of an extension id (x-cap.*) is not trusted for any of them.
CORE-022: mhs.resource.write is a command of the resource's cap.device.configure.v1 binding (PROTO-025, PROTO-025a). The patch is that binding's input; its effective input schema is the set of non-readOnly properties of the resource's state_schema, each keeping its type, x-mhs-unit and bounds. A key outside state_schema is rejected with MHS.RESOURCE.FIELD_UNKNOWN, a readOnly key with MHS.RESOURCE.NOT_WRITABLE (details.fields). Both rules hold at any depth, through allOf, anyOf, oneOf, then, else, local $ref, patternProperties and the draft-07 tuple form of items; a readOnly root or an unresolvable $ref is NOT_WRITABLE. A resource with no configure binding, or a request naming another capability_id, is the JSON-RPC error MHS.RESOURCE.NOT_WRITABLE (details.reason configure_unbound or write_class_not_configure): the patch is never passed as another class's input. Every write receipt, refusals included, carries capability_id cap.device.configure.v1; a completed one has output {applied, revision}.
CORE-023: mhs.command names capability_id, input and command_id. When the class is bound to several resources of the device, resource is required (omitting it is MHS.PROTOCOL.INVALID_PARAMS, details.reason resource_required, details.resources); a resource the class is not bound to, including one the device lacks, is a rejected receipt MHS.CAPABILITY.NOT_BOUND with details.bound_resources. An unknown class is a rejected receipt MHS.CAPABILITY.UNKNOWN (PROTO-014). input is validated against the binding schema intersected with the class schema (PROTO-027).
CORE-024: Grants are per binding. capabilities[] of mhs.session.open takes class ids and patterns (every binding of the class) or objects {capability_id, resource} (one binding; an unbound resource is MHS.CAPABILITY.NOT_BOUND). granted[] has one entry per granted binding, and a grant never widens after open (SESS-001, SESS-011). A permit names one binding: mhs.safety.permit without resource on a class bound several times is INVALID_PARAMS resource_required, and a resource the class is not bound to is MHS.SAFETY.PERMIT_INVALID (details.bound_resources).
CORE-025: A stop class bound to several resources is a command like any other: without resource it is resource_required. The catalogue e-stop cap.motion.estop.v1 and mhs.safety.estop are the exception (CORE-074).
# runnable
from mhs.adapter import load
from mhs.testing import client_for
server = load("MHS/sdk/examples/thermostat", backend="fake") # strict; a fake backend is served as mode "simulated"
client = client_for(server)
device_id = client.hello()["devices"][0]["device_id"]
session = client.open_session(device_ids=[device_id])["session_id"]
read = client.read("main", device_id=device_id, session_id=session) # read
assert read["revision"] == 0 and "temperature" in read["state"]
write = client.invoke("cap.device.configure.v1", {"setpoint": 40}, device_id=device_id,
session_id=session) # write: a configure command
assert write["status"] == "completed" and write["output"]["applied"] == {"setpoint": 40}
action = client.invoke("cap.thermal.setpoint.v1", {"target_temperature": 30}, device_id=device_id,
session_id=session) # action
assert action["status"] == "completed" and action["state_revision_after"] == write["state_revision_after"] + 1
refused = client.invoke("cap.thermal.setpoint.v1", {"target_temperature": 400}, device_id=device_id,
session_id=session) # a limit, not a schema error
assert refused["status"] == "rejected" and refused["error"]["mhs_code"] == "MHS.SAFETY.ENVELOPE_VIOLATION"
server.close()3. The reference file
CORE-030: Every device has a reference file: reference.json, validated by the closed schema reference.schema.json, and REFERENCE.md, its human view. It is generated, never hand-written, and deterministic: the same inputs give the same JCS bytes. Members: openmhs, device, measures, adjusts, actions, safety (estop, safe_state, interlocks, watchdog_ms, operator_required, not_certified: true), facts, tags, fidelity, overlay, provenance. Measures are the non-adjustable state properties and the read bindings that need no input; adjusts are the writable state properties of a resource bound to cap.device.configure.v1, each with its effective limit or operator_required; actions are every other binding. A risk in the file is never below the class floor or the device's declared risk.
CORE-031: The reference file shows the limits the endpoint enforces, not the limits the author hoped for: it is built from the effective limits of CORE-042. mhs.devices.get returns it as reference, with each binding's x-mhs-effective-limits (limits, cumulative, operator_required, unlimited, narrowed); when it cannot be rendered (for example an uncatalogued class) x-mhs-reference-error says why. The MCP tool descriptions and the CLI describe print the same limits and facts (CORE-133, CORE-136).
CORE-032: A fact has a text, a source (vendor_doc, upstream_code, operator or interview) and, for the first two, a ref of the form path/in/clone:LINE at the pinned commit. A statement without both is not a fact. A model-authored driver writes only vendor_doc and upstream_code facts and limits (DRV-030).
4. Limits
One grammar and one engine (reference: mhs.limits) serve classes, bindings, device envelopes, operator overlays and driver declarations.
CORE-040: A limit rule is a closed object {min, max, allowed, unit, reason, source, ref, names, per}; any other member is a load error, so a typo can never remove a bound. min and max are numbers, or one number per element of a fixed-length array (the bound array's length MUST equal the field's length); a scalar bound applies to every element of a numeric array. unit MUST equal the field's x-mhs-unit. source is vendor_doc, upstream_code, class_default or operator, and the first two REQUIRE a ref. names labels array elements in violations. per (session, rolling_1h, rolling_24h, rolling_7d) turns max into a cumulative budget (CORE-047). The literal operator_required in place of a rule defers the bound to the operator overlay. Integer bounds beyond ±(2^53−1) are refused.
CORE-041: A limit path is field, a.b, a[*] (every element) or a[3] (one element), combined as in a.b[*].c: identifier segments joined by dots, each optionally followed by [*] or [N], at most 256 characters (defs.schema.json limitPath). For cap.device.configure.v1 a path is a state-property path. A path that does not resolve in the effective input schema is a load error (CORE-043). A scalar bound on a numeric array applies to every element.
CORE-042: For each binding the effective limits are the intersection of four sources: (1) the class limits (class_default), (2) the binding's limits, (3) the device envelope safety.envelope, (4) the operator overlay. The effective maximum is the smallest maximum, the minimum the largest minimum, allowed the intersection, and element names must agree. Envelope keys are capability ids (cap.<class>.v<N>, x-cap.<owner>.<name>.v<N>) or patterns (cap.<class>.*, cap.*); every key that matches a binding applies. The overlay is site-limits.json (site-limits.schema.json, keyed by device id and then by capability id or parameter path). It is narrow-only: it MAY fill an operator_required bound and MAY tighten any other, and MUST NOT widen anything the regular sources allow. Several narrow-only sources intersect among themselves, so their order never changes the result. There is no other way to relax a limit; an operator bound never lives in a driver (DRV-021).
CORE-043: These defects are load errors in every mode; the endpoint does not start (strict mode reports limits-invalid in MHSStrictError; compatibility mode raises mhs.limits.LimitConfigError): a path that does not resolve; an unknown rule member (a misspelt maxx, the legacy limit); a bound on a non-numeric field or an object; a per-element bound on a scalar or a variable-length array, or of the wrong length; a unit other than the field's x-mhs-unit; contradictory sources; an unknown per window; a widening overlay; an overlay naming an unbound capability or a non-writable parameter; an envelope key that is no capability id or pattern (envelope-key-invalid); an envelope key matching no bound capability (envelope-unbound; compatibility mode exempts the reserved x-cap.mhs-test. namespace); an envelope path that resolves in none of the bindings it matches. After start-up, a device added or an envelope changed with such a defect answers MHS.ADAPTER.MANIFEST_INVALID (details.reason limits_invalid) on every binding except the stop classes; a late device that strict mode would refuse answers MANIFEST_INVALID (details.reason strict_refused). The stop classes are exempt only from such device-level defects: a stop binding's own broken rule refuses that binding, and mhs.safety.estop always works.
CORE-044: The core checks every array element and every nested leaf of the input (or patch) against the effective limits. Absent optional fields are skipped and schema defaults of omitted fields are evaluated. A value that cannot be compared with a numeric bound (a string, a boolean, NaN, an infinity) is a type violation and never passes. A violation is refused before any native call with a rejected receipt MHS.SAFETY.ENVELOPE_VIOLATION. error.details is the first violation {path, kind, bound, value, unit, source} (path is concrete, for example joints[0]; kind is min, max, allowed, type or length; source is class, binding, envelope, envelope:<pattern> or overlay), with element_name, limit_path, reason and provenance when known, violations[] (up to 16) and the legacy field. Limits are compared in the declared unit after conversion (SAFE-014, SAFE-015).
CORE-045: For paths a limit covers, the schema's own numeric bounds are evaluated after the limits (CORE-060 steps 7 to 9). An out-of-range value on a limited field is therefore a safety refusal (ENVELOPE_VIOLATION), not MHS.CAPABILITY.INPUT_INVALID, also for mhs.resource.write.
CORE-046: A call that sets a value, or relies on a schema default, at an operator_required path no overlay has filled is refused with MHS.SAFETY.LIMITS_UNSET (details.paths, details.reason), in every mode and per call. In strict mode, a hardware or hybrid write or invoke at risk R1 or above that sets a numeric leaf no limit covers is refused the same way (details.reason no_limit). Stop classes are never refused for a missing bound, and neither is a catalogued read class; the declared verb of an extension id is not trusted.
CORE-047: A cumulative budget (per) is reserved atomically before the effect and shared across the sessions of the endpoint. The amount drawn is the sum of the magnitudes of the addressed values, so a negative value never refunds a budget. Budgets are pooled per class, the smallest budget wins, and an exceeded budget is MHS.SAFETY.CUMULATIVE_LIMIT (details.used, details.limit, window) (SAFE-016). Stop classes and safe_state.achieved_by actions are exempt. With a state_dir, draws and releases are journalled and a restart never refills a window (CORE-170).
CORE-048: A binding's clamp is a limit and uses the same engine: an input outside it is rejected with MHS.SAFETY.ENVELOPE_VIOLATION before the native call. Nothing is ever truncated (ADPT-024). A clamp's unit is the unit after conversion; a mismatch is a load error.
5. Evaluation order
CORE-060: The core MUST evaluate a command in this order and refuse at the first failure with no side effect. A refused command never consumes a permit (one already taken is given back), never advances a revision and never becomes the recorded outcome of its command_id (SAFE-018, SAFE-019, OP-005). A check that cannot be evaluated refuses. This list replaces the order in SAFE-011.
| Step | Check | Refusal |
|---|---|---|
| 1 | Principal. The principal comes from the bearer token; a self-declared human or operator without a token is an agent (CORE-150) | MHS.AUTH.* |
| 2 | Session and grant. The session is live and owned by the caller; the class is granted, then the resolved binding | MHS.SESSION.*, MHS.SESSION.CAPABILITY_NOT_GRANTED (JSON-RPC errors) |
| 3 | Binding. (capability_id, resource) resolves | MHS.CAPABILITY.UNKNOWN, NOT_BOUND, resource_required |
| 4 | command_id. A replay returns the retained receipt with replayed: true; another input digest, or another action in flight, is refused | MHS.CONFLICT.COMMAND_ID_REUSE |
| 5 | Latch. Effectful commands are refused while the device is latched (CORE-075) | MHS.SAFETY.ESTOP_ENGAGED |
| 6 | Mode, hybrid authority, arming. A replay device refuses every command; the model may not drive a hardware-authority resource of a hybrid device; hardware actuation needs an armed device | MHS.MODE.READ_ONLY, MHS.SAFETY.ENVELOPE_VIOLATION (details.authority), MHS.AUTH.HARDWARE_DISABLED (details.reason not_armed) |
| 7 | Structure. The served schema (binding ∩ class) without the bound keywords of limited fields; configure field rules (CORE-022) | MHS.CAPABILITY.INPUT_INVALID, MHS.RESOURCE.FIELD_UNKNOWN, NOT_WRITABLE |
| 8 | Limits. Every element and leaf (CORE-044), then LIMITS_UNSET (CORE-046) | MHS.SAFETY.ENVELOPE_VIOLATION, MHS.SAFETY.LIMITS_UNSET |
| 9 | Remaining schema bounds | MHS.CAPABILITY.INPUT_INVALID |
| 10 | Permit, bound to the exact input digest, then cumulative budgets (CORE-047) | MHS.SAFETY.PERMIT_INVALID, APPROVAL_REQUIRED, CUMULATIVE_LIMIT |
| 11 | Interlocks. Every gating interlock reads closed; unknown counts as open | MHS.SAFETY.INTERLOCK_OPEN |
| 12 | Device lock, revision, dispatch. Non-read bindings of hardware and hybrid devices hold the device lock unless the binding declares constraints.concurrency shared; expected_revision is compared with the live revision after the lock and again when the operation is registered; then the bridge call, output validation (strict), the receipt | MHS.CONFLICT.REVISION, MHS.DEVICE.BUSY |
CORE-061: Steps 1 to 11 call the bridge only to read interlock and device state, each read bounded. A command whose deadline_ms has passed during the checks is rejected with MHS.OUTCOME.TIMEOUT (details.reason deadline_exceeded_before_dispatch, retryable) and nothing is dispatched (OP-012). The effective concurrency of a binding on a hardware or hybrid device is the stricter of its constraints.concurrency and the class value (default exclusive for non-read bindings; reads are shared unless declared exclusive or serialized; serialized holds the device lock); stop classes and achieved_by actions never wait for the lock and publish shared. Manifests publish the effective value (exclusive in device summaries), and the manifest of a simulated device invents none. A request that finds an exclusive device busy is refused MHS.DEVICE.BUSY rather than queued, and the device stays busy until the endpoint's own state reads around the command have returned.
6. Safety duties of the core and of the bridge
6.1 The split
CORE-070: The core owns, unconditionally and before any bridge I/O: principals and grants, binding resolution, idempotency, the e-stop latch and its persistence, mode, hybrid authority and arming, structure, limits and budgets, permits, interlock evaluation (fail closed: an unreadable or unrecognised state is unknown, which blocks like open, SAFE-012), the device lock, revisions, receipts, the evidence chain, cancellation and session expiry. Only the rules CORE-160 lists as strict-only depend on the mode of the endpoint.
| Concern | The core (endpoint runtime) | The bridge or driver |
|---|---|---|
| Limits | Intersects the sources, checks every element and leaf before any native call, refuses with details (CORE-042 to CORE-046) | Declares bounds with a source and a reference; never validates, clamps or relaxes |
| Units | Compares in the declared unit | Converts native units at the boundary |
| E-stop | Latches first, cancels running work, runs the stop path within the budget, persists the latch (CORE-074 to CORE-077) | Provides the native stop; returns promptly; takes no lock a running call holds |
| Cancellation | Requests it on e-stop, session expiry, operations.cancel and max_duration_ms | Observes it and raises Cancelled |
| Interlocks | Reads them, fails closed, refuses (CORE-073) | Reports closed, open or unknown |
| Permits and budgets | Validates, consumes and gives back permits; reserves budgets | Nothing |
| Receipts, revisions, idempotency | Owns them (CORE-090 to CORE-096) | Returns the output or raises a signal |
| Failures | Classifies the signal into a receipt (CORE-071) | Raises DriverFailed, Busy or OutcomeUnknown; never hides a native failure |
| Mode and fidelity | Serves a fake as simulated, enforces mode and arming (CORE-111, CORE-153) | Provides an honest fake and a fidelity contract |
| Reference file | Renders it from the effective limits (CORE-030) | Supplies tags and facts, each with a source |
CORE-071: A bridge or driver translates and MUST: return the capability output as a JSON-able object, which is the receipt output (never an {"output", "state"} wrapper); expose state only through read; report every outcome that is not a success by raising a signal and never through an output field. The signals are Cancelled (the call stopped because cancellation was requested), DriverFailed(code, message) (a failure it understands, with a registered code), Busy (refused, nothing actuated) and OutcomeUnknown (the effect may have happened, for example a native timeout). Any other exception becomes failed MHS.ADAPTER.SHIM_FAILED with the message driver raised <Type>; the native message is never forwarded. A bridge MUST bound every long or awaitable native by the smaller of its timeout_ms and the remaining execution budget and observe cancellation; an expired bound is an unknown outcome, never a retryable failure. A bridge MUST NOT clamp, retry an actuation, swallow a native failure, advance a revision, make a receipt, or decide a refusal that belongs to the core. NaN and infinite inputs never reach a native call.
CORE-072: A bridge's estop MAY be called from another thread while a call runs. It MUST NOT take a lock a running call holds and MUST return promptly. Each estop advances the bridge's e-stop epoch before it touches the device; an effectful call that was already waiting for a bridge lock when the e-stop fired is refused MHS.SAFETY.ESTOP_ENGAGED once it gets the lock, so no queued actuation follows an e-stop. Calls made after estop returned run normally: the latch belongs to the core. reset, interlocks ([{id, state, description}], state closed, open or unknown; anything else is unknown) and safety_status are optional hooks that supply facts; the core decides.
CORE-073: An interlock has a stable id, a description, a source (hardware, software, operator, simulated) and optionally required_for, the capability ids or patterns it gates. When present required_for MUST be non-empty: an interlock that gates nothing omits the member and is informative (SAFE-020), and mhs.safety.status reports every interlock, gating or not (SAFE-026). A gating interlock that reads open or unknown refuses the command with MHS.SAFETY.INTERLOCK_OPEN and error.interlocks. An interlock that opens while an operation runs stops the affected output (SAFE-013). No method sets an interlock (SAFE-027).
6.2 E-stop
CORE-074: mhs.safety.estop and mhs.command on a bound cap.motion.estop.v1 share one path, which latches first (SAFE-040). (1) Under the endpoint lock and before any bridge I/O the core sets the device's latch, persists it when there is a state_dir, sets cancellation on every running operation of the device (reason emergency stop: <reason>) and revokes the device's permits (SAFE-036). (2) It runs the stop path in a bounded worker: the native stop first (the bridge's estop, else a bound cap.motion.estop.v1, else none), then every safe_state.achieved_by action, which starts no later than the device's e-stop budget safety.estop.latency_ms (1000 ms when undeclared) even if the native stop has not answered, then the bridge's own safe-state report. (3) It answers within that budget and never past the caller's deadline_ms. The path follows the bindings served at e-stop time; a capability stop path with no binding left is SAFE_STATE_UNREACHED. Every achieved_by action is called. On the SDK's own ROS call path (the SDK's RosBridge itself, or a RosDriver whose call, make_bridge, inner, estop, safety_status and _run_safe_state are the SDK's and whose inner bridge is exactly the SDK's RosBridge, with none of them shadowed on an instance), a safe-state command whose request (service or action, type, profile, literal request, success predicate, timeout) the native stop or an earlier action of the run already sent and saw confirmed successful is not sent again: it is answered from that request's response and checked against its own output contract, and the receipt lists it in x-vendor.shared_stop_requests. After a failed, timed-out or unconfirmed request the command sends its own at once; a request still in flight is awaited until the command's budget is down to its own request's timeout or to half, whichever is less, and the command then sends its own unless that request succeeded. The native stop itself always sends. A background retry (CORE-077) is a new run. Other bridges, packages that override that call path and decorated drivers send every action's request (a native method that the @estop method and an action both reach runs twice). A stop of several devices (CORE-076) runs this path once per device, concurrently.
CORE-075: While a device is latched, every effectful command and mhs.restore is refused with a rejected receipt MHS.SAFETY.ESTOP_ENGAGED, before schema validation and without a bridge call; the check is repeated atomically when the operation is registered, so a command that passed its pre-checks before the e-stop does not run after it. Exempt are catalogued read classes, the stop classes (cap.motion.stop.v1, cap.motion.estop.v1) and the safe_state.achieved_by actions (SAFE-029), which also skip permits, budgets and the device lock; their schema, limits and interlocks still apply. An extension id is exempt only as an achieved_by action, never as a read. Known limitation: an achieved_by action that takes input is callable while latched with any input that passes its schema and limits (open item in the 0.9 change notes, MHS/changes/WP-09.md).
CORE-076: The e-stop needs no session, no prior mhs.hello, no valid deadline_ms, no ULID command_id and no mode. An unknown, expired or missing session gets a transient session; a malformed command_id is replaced; a missing or invalid deadline means the device's budget; NaN and infinity in an e-stop request become null and the e-stop is still served; an e-stop sent as a JSON-RPC notification still latches. It is never refused by the latch, permits, budgets, rate limits, arming or the generic gates (the mhs.hello and deadline_ms gates). A request is an e-stop by what it asks for, not by how well its envelope is written: a JSON object whose method is exactly mhs.safety.estop (or mhs.command naming cap.motion.estop.v1 in object params) latches although its jsonrpc is not "2.0", its id is not a string or an integer, or its params are not an object. params that cannot be used, and a device_id that names no device, are ignored in that case, so the stop covers every device; the defect is recorded in the stop's reason. The answer is still the error of the defect (MHS.PROTOCOL.INVALID_REQUEST, or MHS.PROTOCOL.INVALID_PARAMS for params; id null when the id is unusable; no answer to a notification), and its error.data.details say whether the stop was served (estop_served) and carry its receipt. Text that is not a JSON object with that method (not JSON, an array that does not hold one, a number) cannot be recognised and is refused as before. A well-formed request stops the device it names; else the single device of its session; else the only device of the endpoint; else, on an endpoint that serves several devices, all of them. A stop of several devices engages them concurrently (a stop path that hangs on one never delays the latch of another) and answers with one receipt: the first that is not completed, else the first, with x-vendor.estop = {scope: "all_devices", devices: [{device_id, status, receipt_id, safe_state_achieved, error?}]}. It is completed only when every device's stop succeeded (SAFE-002). A device_id that is well-formed but unknown is MHS.DEVICE.UNKNOWN and stops nothing. Over HTTP with auth_tokens configured the e-stop needs the same bearer token as every route (CORE-181): a request without it is refused with 401 before it is decoded, malformed or not.
CORE-077: The e-stop receipt is completed only when every step succeeded. A timeout, any exception, a failed or unbound safe-state action, the bridge reporting achieved: false, or a hardware or hybrid device with no stop path at all gives a failed receipt MHS.SAFETY.SAFE_STATE_UNREACHED with details.reason timeout, native_failed, action_failed, safe_state_unbound, no_stop_path, bridge_reports_unsafe or command_in_flight (CORE-086); the latch stays engaged. mhs.safety.estop never answers MHS.OUTCOME.TIMEOUT. A retryable failure is retried in the background (0.25 s doubling to 5 s) until the stop path succeeds or the device is reset; the success is appended as a completed cap.internal.safety_estop.v1 receipt whose output.confirms names the failed one. estop.confirmed in safety.status means the current attempt of this latch generation succeeded. A sensed-confirmation stop (CORE-088) adds no reason: its unconfirmed outcome is bridge_reports_unsafe (or timeout, or native_failed) with details.sensed saying why, and its completed receipts carry x-vendor.sensed_confirmation. On a simulated or replay device with no native stop, the software-only latch is completed (output.estop software_only). The receipt carries latency_ms, latch_latency_ms, safe_state_achieved and cancelled_operations, and never claims more than the declared safe state (SAFE-044).
CORE-078: Cancellation by an e-stop is cooperative: a cancellable operation ends cancelled; stop-class and achieved_by operations are never cancelled. An effectful operation that ignores cancellation keeps running (logged) and blocks the reset (operation_running).
CORE-079: A stdio or MCP reader handles the e-stop and operations.cancel before it reads the next line, on a priority path that answers while another request is in flight (CORE-132); other requests are served in order, per device lane.
6.3 Reset
CORE-080: mhs.safety.reset is refused with a rejected receipt MHS.SAFETY.RESET_REFUSED, without calling the bridge and with the latch kept, when: nothing is latched (details.reason not_latched; "latched" means the endpoint latch, or a latch the device reports through safety_status() latched or estop.engaged, or a boolean latched state field); an effectful operation that ignored cancellation is still running (operation_running); any declared interlock not marked reset_exempt reads open or unknown, an unreported or unreadable one included (interlock_open, with error.interlocks); a native e-stop attempt of the device is still in flight (stop_path_pending; there is no operator override, and a bridge whose estop never returns keeps the device latched until restart) (SAFE-003, SAFE-041, SAFE-043). It is also refused, in the same way, after the endpoint started the stop path of a declared terminal e-stop (terminal_estop, CORE-087).
CORE-081: The latch is released only when no e-stop was engaged during the reset and no stop attempt is running. A bridge refusal becomes RESET_REFUSED (bridge_refused, bridge_code) and a non-MHS exception a failed receipt, both with the latch kept. A refusal after bridge.reset() ran is a failed RESET_REFUSED receipt (details.latched: true, bridge_reset completed) and advances the revision: estop_during_reset (the stop path is re-asserted, details.stop_reasserted) and stop_path_pending. A reset whose caller was answered TIMEOUT before the handler reached bridge.reset() is not performed: failed, MHS.OUTCOME.TIMEOUT, details.reason deadline_exceeded, bridge_reset not_called. While a reset runs, the background retry of a failed stop does not start a new attempt, at most until that reset's deadline; an e-stop engaged during the reset lifts that hold.
CORE-082: A completed reset clears and persists the latch, ends the background retry and never resumes anything (output.resumed: false); a new command is required (SAFE-042). On a device that declares a re-enable, that command is the re-enable, and output.reenable names it (CORE-086). mhs.restore is refused ESTOP_ENGAGED while latched, is re-checked after its rollback read, and an e-stop landing during it re-asserts the stop path afterwards.
CORE-083: Only an operator releases a hardware latch. On a hardware or hybrid device, mhs.safety.reset from a session that is not an authenticated operator is refused MHS.AUTH.FORBIDDEN (details.reason operator_required, bridge not called) in strict mode; the CLI reset needs the operator token and an interactive terminal, and the MCP projection does not list the tool (CORE-130). On simulated devices an agent may reset.
CORE-086: A device whose stop leaves it disabled MAY declare safety.estop.reenable: the capability that re-enables it after a completed reset, the new start command of SAFE-042. The named capability MUST be bound to exactly one resource of the device and MUST accept an empty input (the empty object validates against its input schema), and it MUST NOT be a read, a stop class or a safe_state.achieved_by action; an endpoint refuses to start a device whose declaration breaks this, in every mode, and refuses a device with such a declaration that was added after start on its first use (MHS.ADAPTER.MANIFEST_INVALID, details.reason estop_recovery_invalid; the e-stop still reaches it). The re-enable is an ordinary effectful command: while the device is latched it is refused MHS.SAFETY.ESTOP_ENGAGED (CORE-075), and arming, permits, limits and interlocks apply to it. A reset of such a device only releases the latch: neither mhs.safety.reset nor the bridge's reset runs the re-enable or re-arms the device by any other means, any native recovery it needs (fault clearing included) belongs in the re-enable, and a completed reset names it in output.reenable. The SDK enforces what it can see, at definition and at start-up: a decorated driver that declares a re-enable MUST NOT define an @reset hook, and a family driver's safety_bindings.reset MUST NOT call the re-enable's native target (the target members of both entries, compared exactly as declared). A family reset binding that re-arms the device through another target, or through the same one spelled differently, is not detected: avoiding it is the author's obligation, checked by review. On such a device, a command already dispatched when an e-stop lands may land after the native stop and enable the device again (the re-enable itself, or an upstream cancel path that re-arms after its own quick-stop). The stop path therefore runs at once, waits for every effectful command of the device still in flight then (stop, safe-state and read actions excepted, reads as the latch counts them) to settle, and runs again; until that second run succeeds the e-stop is not confirmed (a failed receipt MHS.SAFETY.SAFE_STATE_UNREACHED, retried in the background as CORE-077 describes; details.reason is command_in_flight, with details.awaiting_operations listing them, while one of those commands has not settled, and timeout once they all have and the stop that follows has not answered). The wait for a command ends when it settles or when its worker is gone without settling (it can no longer reach the device). A device that declares none keeps the single run of the stop path and needs no separate re-enable: its next ordinary command is the new start command.
CORE-087: A device whose native stop cannot be undone without a new native session MUST declare safety.estop.terminal, one sentence that says why, and its reset MUST NOT reopen or reinitialise it (SAFE-048). A new native session is any reconnection, reopening or re-initialisation that may change the device: energise it, enable motion, or start a process that may initialise hardware. Re-establishing communication that changes nothing on the device (reconnecting a socket, re-attaching a client object, reopening a handle) is not one, and a reset MAY do it (SAFE-042). The declaration needs a stop path (a native e-stop or a safe_state.achieved_by action) and excludes reenable; an endpoint refuses to start a device whose declaration breaks this, in every mode. Once the endpoint starts the stop path of such a device, every later mhs.safety.reset of the device on that endpoint is refused with a rejected receipt MHS.SAFETY.RESET_REFUSED (details.reason terminal_estop, details.terminal the declared sentence, details.latched true, and a remediation), without calling the bridge, and the latch is kept for the rest of the endpoint's life; a reset during whose bridge reset such a stop path starts never releases the latch either (a failed refusal, estop_during_reset or terminal_estop, CORE-081). mhs.safety.status then reports estop.terminated: true. The latch, the refusals of CORE-075 and the background retry of CORE-077 are unchanged. Recovery is a new endpoint: a latch that it restores from state_dir (CORE-171) is reset as usual, by an operator on a hardware device (CORE-083), until a stop path starts on that endpoint, including the rerun that a replay of the latching e-stop's command_id causes (CORE-171); from then on that endpoint refuses the device's resets too.
CORE-088: A device of mode simulated whose only stop is a request (a land, mode, zero-velocity or motor-cut request whose acknowledgement proves nothing) MAY declare a sensed-confirmation stop, published as safety.estop.confirmation (kind sensed, SAFE-049); a device of any other mode MUST NOT, and an endpoint refuses to start one that does, in every mode (MHS.ADAPTER.MANIFEST_INVALID, details.reason sensed_stop_not_simulated), and refuses a late one on first use (the e-stop still reaches it). What the endpoint serves grants the stop, never a value its constructor was given: a confirmation commits a safe state only on the SDK's own ROS bridge after the endpoint bound it to a device whose served manifest declares mode simulated and the confirmation (at start-up, or on first use of a late device), and only on the SDK's route (a RosDriver that overrides none of call, make_bridge, inner, estop, safety_status, _run_safe_state, and whose instance builds no bridge of its own); an unbound bridge still sends the request but never confirms. The bridge's estop sends the declared request steps (one to three fixed service calls with their success predicate, or fixed topic publications; each MUST be idempotent: repeating it after the safe state was reached undoes nothing) on its safety lane, where they are not fenced by the latch or the e-stop epoch and count as no native activity; every step is attempted and its outcome (succeeded, refused, unknown, published, not_sent) recorded, and at least one must have been transmitted. It then opens new subscriptions to one to three sources, discards what each may have received from before it existed (on ROS 1 the bridge has no proven offset between the producer clock and its wall clock, so a producer stamp is not a freshness proof: every ROS 1 stamp sample is discarded and does not refresh counted freshness, including a stamp one second ahead, a ROS simulation clock, a delayed latch, a frame already in a client buffer, and a sample from a transport that cannot report its buffer; an empty queue, a socket poll, a client library buffer, a continuation flag, and a fixed lead are not that proof; on ROS 2 the publisher's declared history, volatile or transient_local with its depth, cited or operator-supplied and never lower than a cited one, and at least one sample), numbered by arrival before any is dropped. A stamp source then counts samples with strictly increasing producer stamps. A ROS 2 source whose producer does not stamp may instead declare freshness arrival with a cited volatile history and min_interval_ms (1 to 1000): the first sample is discarded, a later sample closer than min_interval_ms restarts the count, and the window's span is arrival time; transient-local history and ROS 1 are refused for that freshness, because a replay spaced at least min_interval_ms apart cannot be told from live samples without a stamp. Counted samples are those whose every predicate term holds (equals, range, abs_max, norm_max, span_max over the window); a repeated stamp, an arrival gap above max_gap_ms measured from the newest counted sample, or a failing term restarts the count, and a discarded replay does not advance that arrival, and a missing, malformed, zero or decreasing stamp, or a missing, non-numeric or mistyped field, ends the attempt. When the declaration names an exclusion on a request topic, silence, a subscription error, a sample whose watched fields are not all zero, and a sample the exclusion ring drops each refuse the confirmation. A dropped sample was not inspected, so the tail that remains is not a passive command. Only a passive sample observed on that subscription after it opened, and still fresh at the commit, is evidence the competing command is absent. The safe state is committed, and estop returns safe_state_achieved: true, only when that exclusion rule holds and every source holds consecutive (at least 2) counted samples spanning min_span_ms of producer time (arrival time when freshness is arrival) whose newest arrived within max_gap_ms, before confirm_within_ms after the first transmission and the caller's deadline, with no native activity, new e-stop epoch, disconnect or close in between; otherwise it returns unachieved with confirmation.reason request_not_sent, sensed_missing, sensed_stale, sensed_invalid, not_confirmed, superseded (a closed bridge or a lost connection included) or not_simulated (the bridge is not bound), and never raises for an unconfirmed outcome. safety_status reports the committed confirmation of the current attempt only (safe_state.confirmation), withdrawn by any later native activity, epoch, disconnect or close. The core is unchanged (CORE-074, CORE-077): the latch is set first; a failed e-stop carries details.sensed, and a completed e-stop or background confirmation x-vendor.sensed_confirmation, both copied only from the SDK's own ROS route (CORE-074); the background retry sends the request again. A reset of such a device first re-confirms read-only (new subscriptions, no request, no re-enable) and is refused (bridge_refused, latch kept) when the sensed state no longer holds; it never resumes anything (SAFE-042). Thresholds are cited or operator-supplied (the operator's are narrow-only against a cited value), and so is each ROS 2 source's history (the operator's may only raise a cited replay); consecutive, min_span_ms, max_gap_ms, confirm_within_ms, throttle_ms and the request timeouts are client execution bounds that can only withhold a confirmation; safety.estop.latency_ms covers the request timeouts plus confirm_within_ms. A stop that only a new native session undoes also declares safety.estop.terminal (CORE-087).
6.4 Permits, sessions and expiry
CORE-084: A permit is single-use, bound to the exact input digest, binding, session and principal, and expires (SAFE-030 to SAFE-036). In strict mode a permit on a hardware (or hybrid, hardware-authority) device at R2 or above needs the endpoint's approval_verifier; without one it fails closed with MHS.SAFETY.APPROVAL_REQUIRED. A permit consumed by a command that is then refused before dispatch (a revision conflict, an expired deadline) is given back, unless an e-stop revoked it. A model must not be able to approve its own permit: the MCP projection hides the permit and reset tools unless an operator enables them (CORE-130), and a host collects approvals through an operator route (HOST-040 to HOST-047).
CORE-085: On session expiry or close the core cancels the session's running operations (stop and achieved_by operations excepted), releases its permits and subscriptions, and runs the safe_state.achieved_by actions of every device that was running one of its effectful operations (SESS-004, SESS-006, SESS-010). Expiry relies on cooperative cancellation plus safe_state.achieved_by: a device without achieved_by whose bridge ignores cancellation keeps running (this is logged). The session record stays, so later use, a heartbeat included, gets MHS.SESSION.EXPIRED.