By nghxni

Technical article

Robot shared safety policy: denylist, allowlists, and fail-closed semantics

LightESB-Camel enforces robot command safety at the management API, not in each route: validate and submit share the same policy snapshot, denylist policies isolate by site, robot, or protocol profile, and every rejection path fails closed — no command, no outbox, no protocol call.

Safety rules belong to the control plane, not to each route

The preflight endpoint POST /service-management/v1/robots/{robotId}/commands:validate and the formal POST .../commands run the same high-level safety rules: move_to checks the target area and requested speed, and configured pick/place checks the station allowlist, server-side interlock, and payload. Area, speed, station, interlock, or payload rejection returns 422 ROBOT_POLICY_REJECTED; an illegal numeric type returns 400 ROBOT_COMMAND_SCHEMA_INVALID.

A passed validate only means the current server-side policy snapshot accepts the command — the field map, the PLC/controller safety loop, and actual execution are not verified. Formal submission re-runs the same policy; on rejection no command and no outbox is created.

One gate, three configuration layers

Shared safety rules

move_to validates target area and speed; pick/place validates the station allowlist, server-side interlock, and payload. Validate and submit both evaluate the same server-side policy snapshot, so the preflight answer is the answer submission would give.

Denylist policies

ROBOT_POLICY_DENYLIST is maintained through the management API, never in route XML. scopeType supports site, robot, and protocolProfile, with one record per scopeType + scopeValue. A hit on an enabled policy returns 422, triggers no protocol call, and the rejection detail carries policyId and disabledSource.

Allowlist boundaries

AI inference allowedCommands must be a subset of robot.command.allowedActions; approvers are restricted by allowed-approver-ids; and request bodies may not carry dynamic protocol target fields such as topic, mqttTopic, node, register, service, broker, endpoint, unitId, or functionCode.

Fail closed by default

Rejection creates no command and writes no outbox. The AI one-shot submit re-verifies the candidate digest, denylist, capability, and the current safety policy in a single transaction — any failure rolls everything back. Missing inference configuration fails closed too.

Policy administration is an audited API

Denylist query, add, enable, and disable all go through the management API. Operations should prefer disable; delete exists only to clean up mistakenly created policies. Every add/enable/disable/delete writes a control-plane audit event with robotId=robot-policy and commandId=policy:<policyId>, readable through the robot audit query entry.

POST /service-management/v1/robots/{robotId}/commands:validate
GET  /service-management/v1/robots/policies/denylist
POST /service-management/v1/robots/policies/denylist
POST /service-management/v1/robots/policies/denylist/{id}:enable
POST /service-management/v1/robots/policies/denylist/{id}:disable

Policy and schema failures are separated by error code, which keeps gate decisions and troubleshooting deterministic:

400 ROBOT_COMMAND_SCHEMA_INVALID   schema missing, bad pattern, or illegal numeric type
404 ROBOT_NOT_FOUND                  unknown robot
422 ROBOT_POLICY_REJECTED            denylist hit, or area/speed/station/interlock/payload rejected
422 ROBOT_CAPABILITY_NOT_SUPPORTED   capability unsupported
422 ROBOT_AI_APPROVAL_REQUIRED       AI-reserved commandId submitted through /commands

What the policy gate does not prove

A passed validate does not verify the field map, PLC/controller safety loop, or execution outcome. Route-level mock validation with robotCommandValidateProcessor and robotCommandEnvelopeProcessor — wrapped in a local doTry/doCatch that maps failures to 400/422 — proves the route, configuration, policy, and message envelope only; it does not prove the broker received anything or that the robot moved.

While a denylist policy is active, read-only robot/state queries stay available and return the disabled status, disabledPolicyId, and source. And outboxStatus=pending after an accepted submit only means the command entered the reliable dispatch queue — nothing more.

Command dispatcher APIEdge inference mock guide