5 Safety
This section states what a Safety Kernel MUST do. A Hub at Conformance Level control or full MUST evaluate every command through a Safety Kernel that meets every requirement here.
5.1 Guarantees
- One path. Every command, from any source, MUST be evaluated by the Safety Kernel. Sources include every Client, routines, the dashboard, the driver writer, bridges and the stop command itself. A Driver MUST accept a change only with a single-use Grant the Safety Kernel issued for that exact Device, target and value.
- Deterministic. The Safety Kernel MUST NOT use a model, network calls or randomness to decide. The same state and the same command MUST produce the same decision and the same message.
- Limits come from humans. Agents MAY tighten limits for their own session. Nothing MAY loosen a limit except a human editing the Device File, which records who and when. A Device File with unconfirmed limits loads read-only.
- Stop works. One stop command MUST put every Device in its safe state and refuse every other command until a human unlocks.
- Fail closed. Any unexpected exception inside the Safety Kernel MUST block the command and be logged. Nothing fails open.
- Everything is recorded. Every read request, command, block, confirmation, stop and unlock MUST be appended to an audit log with a hash chain.
5.2 The checks, in order
The Safety Kernel evaluates a command against the current state in this fixed order and MUST stop at the first failure. Each failure has a stable kernel code and a registry name.
| # | Check | Blocks when | Kernel code | Name |
|---|---|---|---|---|
| 1 | Command shape | Unknown Device, channel or action, wrong argument names, malformed payload | unknown_target, bad_request | UNKNOWN_TARGET, INVALID_PARAMS |
| 2 | Stop lock | The global stop lock is set | stopped | STOPPED |
| 3 | File state | The Device File has unconfirmed limits (read-only mode) | read_only_file | UNVERIFIED_LIMITS |
| 4 | Device state | The Driver is not connected, is in a fault state, or is applying its safe state. Also, any registered Device whose expressions name a Device that isn't registered blocks every command on the Hub, because a misspelled id protects nothing | device_unavailable, misconfigured | DEVICE_UNAVAILABLE, MISCONFIGURED |
| 5 | Principal scope | The Principal's kind or scope does not cover this Device's hazard class or this operation | not_permitted | SCOPE_DENIED |
| 6 | Units | The value has no unit, the unit can't be converted to the channel unit, or the result is not finite | bad_unit | UNIT_INVALID |
| 7 | Type and enum | The value is the wrong type, not one of the allowed values, too long, or doesn't match the pattern | bad_value | INVALID_VALUE |
| 8 | Range | Below min or above max, including session limits | below_min, above_max | LIMIT_EXCEEDED |
| 9 | Step | Not a multiple of step from min | bad_step | INVALID_VALUE |
| 10 | Rate | The change from the last known value exceeds max_change_per_s times the elapsed time, the current value is unknown, or an action already ran max_runs times in its window | rate_limited | RATE_LIMITED |
| 11 | Interlocks | An action's require doesn't hold, or an interlock that applies is not satisfied by the current and commanded state | interlock | INTERLOCK_BLOCKED |
| 12 | Watchdog budget | The value would start or continue a state whose max duration is already used up | duration_exceeded | WATCHDOG_TRIPPED |
| 13 | Serialization | Another command holds the Device for longer than 5 s | busy | BUSY |
| 14 | Confirmation | The channel or action needs confirmation, or the Device is high hazard, and there is no valid approval | needs_confirmation, queue_full | CONFIRMATION_REQUIRED, QUEUE_FULL |
Check 13 runs before the others. Commands to one Device MUST run one at a time. A command waits up to 5 s for the Device, and is then evaluated against the state the previous command left.
A command that passes every check gets a Grant and runs. A command that reaches check 14 without an approval goes to the confirmation queue, and hal/write or hal/action returns a pending result with its requestId.
5.3 Unknown values
- A channel's value is unknown when nothing has been read since the Hub connected, when a sensor reading is older than
max(10 s, 3 x heartbeat), while a command to that channel is in flight, after a command to it failed or timed out (it may have landed), and after an action that lists it ineffects. - A write channel's last commanded value stays known while the Device is connected, because the Device holds it.
- The Hub SHOULD read such channels back right away when it can.
- Unknown MUST NOT let a command pass. Interlocks treat it as not satisfied, rate limits block until a fresh value arrives, and watchdog rules keep counting.
- While a command to one Device is in flight, the channels it may change are unknown to every other Device's interlocks.
5.4 Rules for specific checks
- Units. Values are converted to the channel's declared unit with one pinned unit registry.
85 degFagainst amax: 30 degCchannel is converted to 29.44 degC first, then compared. Bare numbers MUST be refused fornumberchannels. - Range tolerance. A value within 1e-12 relative of a limit passes.
- Step tolerance. A value within 1e-6 of one step passes.
- Rate of change. The reference value is the newest known value for that channel, whether the Safety Kernel accepted it or read it from the Device. If nothing is known since connecting, the first command is measured from the channel's safe-state value, or from
minif there is no safe state. If the value became unknown after connecting, the command is blocked until a fresh reading arrives. The allowed change ismax_change_per_s * elapsed_seconds, with elapsed time on a monotonic clock, capped at 3600 s, and a relative tolerance of 1e-9. Commands that fail this check are blocked, not ramped. A Client that wants to move further issues steps that each pass. - String patterns MUST run with a time budget so no value can stall the Safety Kernel. The reference uses 50 ms. A value whose check runs out of time is refused.
- Interlocks use the expression language of Device files and are parsed by the Hub's own parser. Expressions MUST NOT be evaluated as code.
- Session limits. A Client MAY narrow
min,maxormax_change_per_sfor its own session. A request that would widen a limit MUST be refused withcannot_loosen(MISCONFIGURED). This version exposes session limits through MCP and REST only.
5.5 Grants
- A Grant names one Device, one target (
write:<channel>,action:<name>orsafe_state) and one exact value in the channel's unit. - Only the Safety Kernel can create a Grant. Constructing one any other way MUST fail.
- A Grant MUST be consumed on use, and MUST be refused if it was already used, has expired, or names another Device, target or value. The reference expiry is 5 s.
- A Driver receives a verifier bound to its own Device, not a reference to the Safety Kernel.
- A stop and a lost connection revoke every live Grant for the Device.
5.6 Principals and hazard classes
| Hazard | Examples | Agent scope needed | Confirmation |
|---|---|---|---|
low | sensors, LEDs, small fans, displays | control_low | as the Device File says |
medium | motors, pumps, printers, heaters | control_medium | as the Device File says |
high | lasers, CNC spindles, high voltage, high-power heaters, robots that can injure | control_medium to request, never to execute | every command waits for a human, every time |
- An agent with scope
readMUST NOT change write channels or run actions. - A human Principal's own command counts as confirmed. Every other check still applies to it.
- The
systemPrincipal is internal to the Hub. It applies safe states and watchdog actions and is never accepted from a Client. - A routine runs with the scope of whoever enabled it, capped at
control_medium(Routines). - A routine MAY drive a high hazard Device only when the Device File holds a human-signed approval for that routine's content hash (Device files). Changing the routine invalidates the approval.
5.7 Confirmations
- Agents never confirm. A command that needs confirmation is queued as a Confirmation Request.
- A Confirmation Request holds the exact command, the requesting Principal, the dry-run result at the time of the request and an expiry. The reference expiry is 5 minutes.
- Each Principal MAY have at most 20 pending requests, and the queue holds at most 100. A full queue refuses new requests with QUEUE_FULL. It MUST NOT evict another Principal's request.
- Only a human Principal may approve or reject. Other Principals may see the status of their own requests.
- Approval MUST re-run every check against the current state. If the command would now be blocked, it is blocked, not executed.
- Tier
confirmlets one approval cover identical repeat requests from the same Principal for 10 minutes. Tierconfirm-each-timeapproves exactly one execution. High hazard is alwaysconfirm-each-time. - A routine cannot wait for a person. Its command that needs confirmation is blocked with CONFIRMATION_REQUIRED, except on a high hazard Device whose file carries a valid signed approval for that routine.
- Every change of a request's status (expired, cancelled, approved, rejected) MUST be logged. Expired requests are rejected automatically.
5.8 Stop and unlock
- Any Principal MAY stop, including agents. Only a human MAY unlock.
- A stop runs in this order. Set the lock, cancel pending Confirmation Requests, clear approval windows, disable routines, cancel any command still in flight, apply every Device's safe state, then report the result per Device.
- Every safe-state step MUST run even if an earlier one fails. A Driver with native safe-state support runs its own safe behavior first, then the Device File's steps.
- The result per Device is
safe,partial(some steps failed),failed,no_safe_state, orpendingwhen a command was still running and couldn't be cancelled in time. - A command that was in flight during a stop MUST reapply the safe state when it returns, so it can't win.
- Unlock MUST be refused while a stop is still being applied, and clears approval windows again.
- Unlock requires the typed phrase
unlock. The reference CLI also requires an interactive terminal by default.
5.9 Watchdogs
- Heartbeat. The Hub checks each Device at its
heartbeatinterval. On connection loss it applies the safe state whereon_lossasks for it, marks the Device unavailable and blocks commands until it reconnects. - Max duration. A rule's budget starts when its expression becomes true and only resets once the expression has been false for
reset_after, so a brief dip doesn't refill it. While the budget runs, an unknown value counts as true. - When a budget runs out, the Hub applies the rule's
then. A failed action is retried with backoff, and athenmapping that fails three times escalates to the safe state.
5.10 Limits provenance
- A Device File accepts commands only when a human confirmed its limits (Device files).
- A confirmation that carries
sha256is bound to the file's limits, so any later change makes the file read-only again. - A LIMIT_EXCEEDED reason MUST name the Device File and the human confirmation behind the limit.
data.confirmedByanddata.confirmedAtcome from the channel's ownconfirmed, or else the file's.
5.11 Error registry
A blocked or failed command is a JSON-RPC error (Messages). data.name is what Clients branch on. Numbers are stable across versions and MUST NOT be reused. Every kernel code maps to exactly one name.
| Code | Name | Kernel codes | Meaning |
|---|---|---|---|
| -32700 | PARSE_ERROR | Invalid JSON | |
| -32600 | INVALID_REQUEST | Not a JSON-RPC request | |
| -32601 | METHOD_NOT_FOUND | Unknown method, or one above the Hub's Conformance Level | |
| -32602 | INVALID_PARAMS | bad_request | Params don't match the schema |
| -32603 | INTERNAL_ERROR | internal_error | A bug in the Hub |
| -32001 | NOT_INITIALIZED | hal/initialize hasn't run | |
| -32002 | UNAUTHORIZED | Missing or unknown token | |
| -32003 | SCOPE_DENIED | not_permitted | The Principal's token scope doesn't cover this Device or method |
| -32004 | UNKNOWN_TARGET | unknown_target | No such Device, channel or action |
| -32005 | DEVICE_UNAVAILABLE | device_unavailable | Disconnected or not ready |
| -32010 | LIMIT_EXCEEDED | above_max, below_min | Outside the Device File's confirmed limits |
| -32011 | RATE_LIMITED | rate_limited | Faster than max_change_per_s allows right now |
| -32012 | INTERLOCK_BLOCKED | interlock | An interlock or action requirement doesn't hold, or a value it needs is unknown |
| -32013 | CONFIRMATION_REQUIRED | needs_confirmation | Needs a person. data.requestId names the request when it was queued |
| -32014 | STOPPED | stopped | The Hub is stopped until a person unlocks |
| -32015 | UNVERIFIED_LIMITS | read_only_file | The Device File's limits aren't confirmed by a person, so it is read-only |
| -32016 | INVALID_VALUE | bad_value, bad_step | Wrong type, step, enum value or pattern |
| -32017 | WATCHDOG_TRIPPED | duration_exceeded | A watchdog budget is used up |
| -32018 | BUSY | busy | Another command holds the Device |
| -32019 | QUEUE_FULL | queue_full | The confirmation queue is full |
| -32020 | DRIVER_ERROR | driver_error | The Device or its protocol failed, and the channel is unknown until a readback |
| -32021 | MISCONFIGURED | misconfigured, cannot_loosen | The Hub's Device Files are inconsistent, or a change would loosen a confirmed limit |
| -32022 | REQUEST_CLOSED | expired, rejected | The Confirmation Request expired or was rejected |
| -32023 | UNIT_INVALID | bad_unit | A missing, unknown or incompatible unit, or a value that isn't finite |
5.11.1 Reason messages
error.message is formed from data, one template per name. The Safety Kernel's own message stays available as data.kernelMessage. Every reason SHOULD name the rule, the limit and what to do next.
The LIMIT_EXCEEDED templates are normative.
| Kernel code | Template |
|---|---|
above_max | {channel} {requested} exceeds the limit of {limit} ({file}, confirmed by {confirmedBy} {confirmedAt}) |
below_min | {channel} {requested} is below the limit of {limit} ({file}, confirmed by {confirmedBy} {confirmedAt}) |
For an action argument, {channel} is {action} {argument}, as in water duration 120 s exceeds the limit of 30 s. For the canonical example the reason reads extruder_target 300 degC exceeds the limit of 260 degC (printer.hal.yaml, confirmed by owner 2026-10-24).