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}:disablePolicy 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 /commandsWhat 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.