joint_limits accepts a nan bound at construction, then silently drops every inbound joint_command
## Problem
`RosTelemetryBase._validate_joint_limits` exists so a malformed bound refuses the
bridge at construction. Its own docstring states the reason:
> Failing fast here (rather than per-command) means a malformed bound surfaces at
> bridge construction, not as a silent mid-run rejection of every command.
It does not achieve that for a non-finite bound. The only ordering check is
`if low > high`, and **every comparison against `nan` is `False`**, so
`(-1.9, nan)` passes validation. `_command_action` then evaluates
`low <= pos <= high`, which is also `False` for every position, so the bridge
drops **every inbound `joint_command`** for that joint - the exact failure mode
the guard was written to prevent, and the one it reports as a successful
construction.
`inf` bounds pass for the same reason and are equally unusable: `(inf, inf)`
admits nothing.
## Measured
`strands_robots.ros_telemetry.RosTelemetryBase`, in-process, no ROS 2 needed
(both methods are static/classmethod). An in-range command of `0.5`:
| `joint_limits` | construction | command `0.5` |
|---|---|---|
| `{"shoulder_pan": (-1.9, 1.9)}` | accepted | applied |
| `{"shoulder_pan": (-1.9, nan)}` | **accepted** | **dropped, every time** |
| `{"shoulder_pan": (nan, nan)}` | **accepted** | **dropped, every time** |
| `{"shoulder_pan": (inf, inf)}` | **accepted** | **dropped, every time** |
| `{"shoulder_pan": (1.9, -1.9)}` | refused (`min > max`) | - |
```python
from strands_robots.ros_telemetry import RosTelemetryBase as R
nan = float("nan")
print(1.9 > nan) # False <- why the ordering check passes
class Msg:
name = ["shoulder_pan"]
position = [0.5]
norm = R._validate_joint_limits({"shoulder_pan": (-1.9, nan)}) # accepted
print(R._command_action(R, Msg(), joint_limits=norm)) # None
```
The runtime log is honest about the drop but not about the cause - it prints the
`nan` as if it were a declared range an operator chose:
```
rejecting joint_command - shoulder_pan=0.5000 outside declared range [-1.9000, nan]
(whole command dropped, no partial application)
```
## Why it matters
This is the guard on the inbound command surface of a **physical** arm. The
symptom is a live, correctly-constructed bridge that silently ignores every
command from an external ROS 2 stack - and the operator's evidence is a bridge
that came up clean. It is the same class as #1754 (a non-finite value admitted
by a guard that checked coercion but not finiteness, then reporting success);
that one poisoned state, this one silently refuses all actuation.
## Fix
`strands_robots.utils.finite_number_error` is already the shared domain guard for
exactly this - a signed finite physical quantity - with 75 call sites, so this is
a use of the existing rule rather than a new one (AGENTS.md convention 11: a
value-domain guard becomes shared when it has a second caller; this is the Nth).
Apply it to `low` and `high` before the ordering check, so a non-finite bound is
refused with the same wording as everywhere else.
Both hardware bridges (`HardwareRosBridge`, `HardwareRtpsBridge`) inherit this
validator, so one fix covers both transports.
## Test
`tests/` pin: a `nan` bound, an `inf` bound, and `nan`/`inf` on either side each
raise `ValueError` at `_validate_joint_limits`, and the valid-range case still
admits an in-range command. Fails on pre-fix code (currently all four are
accepted).
Found while verifying prose for the ROS 2 section of the fleet-orchestration blog
against the code: the draft claimed malformed limits are refused at build time,
which is what the docstring promises and is true for every malformed bound except
a non-finite one.
0 条评论