Repository navigation
feat(mesh): AckermannRosRobot - Ackermann ROS 2 cars (AWS DeepRacer) as strands robots - #2689
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…stop, scan Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ling-zero stop Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ngelog Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d-command trailing zero, validation before handshake Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
main now assembles CHANGELOG.md from changelog.d/ news fragments, and AGENTS.md forbids appending to ## [Unreleased] directly - that anchor is exactly what made this branch conflict. The entry moves to changelog.d/1035-ackermann-ros-robot.md and CHANGELOG.md is taken verbatim from main, so the branch no longer touches a shared anchor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # docs/ros2-integration.md
…omains
Merging main brought in the structural pin that every mesh class defining
drive() must validate its numeric inputs through the shared domain helpers
rather than a hand-rolled copy. AckermannRosRobot is exactly the fourth
transport that pin was written for, and it shipped the inline math.isfinite
test the helpers replaced:
- bool was treated as a number, so drive(linear=True) published a servo
message commanding 1/1.5 throttle and returned success, and
AckermannRosRobot(max_speed=True) installed a silent 1.0 m/s clamp.
- a value that cannot be coerced raised a bare TypeError, escaping the
structured tool-result contract of the bound drive_<node> agent tool
instead of naming the parameter.
- count was not checked at all: count=0 published nothing and still
reported success, count=2.7 surfaced a raw publish-loop error attributed
to a transport the caller never invoked.
drive() and the constructor limits now call finite_number_error,
positive_finite_number_error and positive_whole_number_error, so the car
refuses an unusable velocity, hold or count with byte-identical text to the
three differential-drive bridges. The max_duration ceiling is ordered after
the finiteness guard because every comparison against nan is false.
The bridge is added to the cross-bridge parity table so the contract is pinned
behaviorally and not only by the AST scan. Two changes make room for it: the
table now carries a keyword-taking constructor, replacing a label dispatch
whose fallback branch would have silently re-checked an already-covered bridge
instead of the new one; and the expected wire fields move into the table, since
the Ackermann car publishes a normalized servo pair where the others publish a
Twist. The servo values are re-derived in the test from the documented bicycle
model rather than read back from the converter under test. Removing the count
guard fails 10 checks in this file, so the added coverage is not vacuous.
Absorbs a base-branch test-fixture fix the branch was missing. The only red
check on this PR was `call-test-lint` failing one isaac test:
tests/simulation/isaac/test_deferred_physics_and_warmup.py::
TestWarmupCameraResumesTimeline::test_resumes_stopped_timeline_and_warms_up
assert sim._warmup_camera("wrist_image", n_steps=5) is True -> False is True
That file is not in this branch's diff (which touches strands_robots/mesh/,
examples/ros2/, docs/ros2-integration.md, tests/mesh/ and one changelog.d/
fragment). The `IsaacSimulation` skeleton those pins drive omitted
`_world_created`; the warm-up loop breaks on the resulting AttributeError and
returns False. `main` hardened the skeleton to set it, so the branch was simply
running the pre-fix fixture - ordinary merge-base staleness on a PR open since
2026-07-03, not a defect in the Ackermann bridge.
Merge is clean: zero conflicts, no hand resolution, no production file touched
by this commit.
Verified in one environment:
- The previously-failing file: 11 passed.
- tests/mesh/test_ackermann_robot.py: 40 passed.
- tests/mesh/test_drive_command_numeric_domains.py (the semantic-clash canary,
since the merge pulls in further structural scanners): 86 passed, 1 skipped.
- Full tests/ failure set diffed against a clean checkout of main in the same
environment: identical, zero regressions (255 failed / 639 errors on both,
all missing-optional-dependency noise), with passes 11624 -> 11665.
# Conflicts: # docs/ros2-integration.md
…n repr The constructor validates node_name via _check_topic before assigning it, so a refused construction left the half-built instance the raising frame keeps alive rendering as [AttributeError ... 'node_name' raised in repr()] - naming the attribute rather than the ValueError the caller already caused. Delegate to the single owner of the fallback wording, strands_robots.utils.partial_construction_repr, exactly as RosBridgedRobot, RosbridgeRobot and RtpsRobot do, and triage the class into the survey in tests/test_repr_survives_partial_construction.py - both the half-built sweep and the documented-refusal cases, for parity with the other ROS/RTPS bridges.
…tory test_the_survey_finds_the_bridges_it_is_meant_to_cover is the non-vacuity half of the drive/stop guard: it pins that the scan finds the bridges it is meant to cover, so a mis-rooted or empty survey cannot read as compliant. A fourth drive/stop bridge therefore has to be triaged there rather than silently widening the set the scan reports. The rule itself already covered the class - the survey walks the mesh package, and renaming its stop_ tool fails test_every_bridge_owning_a_drive_stop_pair_builds_a_stop_tool. It stays out of BRIDGES on purpose: those cases assert a zero geometry_msgs/msg/Twist on /cmd_vel, and an Ackermann car halts with a zero ServoCtrlMsg on its servo topic, which tests/mesh/test_ackermann_robot.py pins.
yinsong1986
left a comment
There was a problem hiding this comment.
Summary
Carries #1035 forward onto current main: a fourth mobile-base transport, AckermannRosRobot, presenting Ackermann-steering ROS 2 cars (AWS DeepRacer reference wiring in from_deepracer()) with the fleet-standard drive(linear, angular, duration) contract, a bicycle-model conversion to normalized ServoCtrlMsg servo pairs, a declarative init_services enable handshake, and a trailing zero-servo message after timed/multi-message commands so a hold cannot leave the throttle latched. The merge also fixes the two guard failures the stale base was hiding (__repr__ on a half-built instance, and the drive/stop survey inventory), triages the class into the shared numeric-domain parity suite, and resolves the one documentation conflict keeping both sides. The two inline comments are both on the new bridge's command path: its motion surfaces sit entirely outside the use_ros operator-approval gate the adjacent doc section documents, and the trailing safety stop's outcome is discarded.
What's good
- Validation goes through the shared
finite_number_error/positive_finite_number_error/positive_whole_number_errordomains, and the parity suite (test_drive_command_numeric_domains.py) was extended to compare the fourth transport against the other three rather than adding a hand-rolled copy. - The
__repr__fix delegates topartial_construction_repr(single owner of the wording) and the class is triaged into both halves of that survey, per AGENTS.md rule 14. - The deliberate exclusion from
BRIDGESintest_bridge_stop_tool_parity.pyis recorded next to the inventory with its reason, and the zero-servo halt is pinned in the bridge's own test file. - Changelog fragment under
changelog.d/, docs conflict resolution keepsmain's safety subsection in the right heading hierarchy, and the atan2 quadrant handling for reverse motion is both explained and tested.
`AckermannRosRobot.drive` sent its trailing zero servo message from a `finally` and discarded the result. `use_ros` reports a transport failure as an error dict rather than raising, so when the main publish succeeded (car driving for `duration`) and the trailing zero then failed - node or daemon dying mid-hold, transport error - the caller saw the drive's `success` for a car still holding the commanded throttle. The tool description promises "a command with duration stops automatically afterwards", so an agent reading that success never issues `stop`, and a servo command latches on this platform. The halt's verdict is now captured and, when the drive succeeded but the halt did not, returned as an error naming the published command, the failed halt and its cause. A failed drive stays the reported cause, and a latching single-shot command sends no halt so there is no verdict to read. Pinned by three cases in tests/mesh/test_ackermann_robot.py, next to `test_drive_trailing_zero_sent_even_when_publish_errors`, which covers the inverse direction: the failed-halt case, the both-failed precedence, and a control that a single-shot success is not reinterpreted. Restoring the discard fails the first.
…gate The bridge's motion commands sat entirely outside the `use_ros` operator-approval gate, in both halves that gate needs. Blocklist coverage. `_gate_command` only fires for a surface matching `_DEFAULT_COMMAND_BLOCKLIST` exactly or by base name, and the shipped DeepRacer wiring matched no entry: `/webserver_pkg/manual_drive` (base `/manual_drive`), `/ctrl_pkg/vehicle_state` and `/ctrl_pkg/enable_state`. Every servo publish and every handshake call therefore proceeded with no prompt, no allowlist check and no audit row - while `/motor_enable` and `/enable_motor` were blocked and the functionally identical `/enable_state`, which is what makes the car act on a servo message at all, was not. `/manual_drive`, `/vehicle_state` and `/enable_state` are now default entries, spelled bare so the base-name rule covers the namespaced spellings. Context threading. Coverage alone would have turned every command, `stop` included, into a fail-closed refusal, because nothing here carried an operator context. `drive`, `stop` and `enable` now take a `tool_context` and thread it into every `use_ros` call, and the `drive_<node>`/`stop_<node>` tools are declared `@tool(context=True)`, so an agent driving the car prompts the operator exactly as the differential-drive bridge does, and a programmatic or headless call is refused with the two variables that lift it named. `docs/ros2-integration.md` (blocklist table, the final-path-segment example list, and a gated-surface table in the Ackermann section), `docs/security.md` and `examples/ros2/deepracer_agent.py` state that posture where an operator sizes a pre-approval. Pinned by tests/mesh/test_ackermann_command_gate.py, which keeps the real `use_ros` and doubles the rclpy transport instead, so the gate and the bridge wiring run together: the prompt order over all three surfaces, a decline at the handshake and at the servo topic, the halt reaching the operator, the trailing halt of a timed drive, both allowlist spellings, and structural guards on `context=True` and on every command call site. `TestEveryCommandingMeshBridgeHasAGateSuite` derives the bridges owing such a suite from the tree, so a fifth one is graded on arrival rather than shipping ungated the way this one did. Removing the three entries fails 7 cases; dropping `context=True` fails 2; dropping one forward fails 5.
The gate is keyed on the graph name of the surface a command targets, so it knows nothing about a transport added later, and a bridge suite that patches the module's `use_ros` symbol cannot see the bypass. Convention 18 states both halves as one change - name the surfaces on the blocklist (bare, so the final-segment rule covers namespaced instances) and thread `tool_context` into every `use_ros` call with the command tools declared `@tool(context=True)` - because either alone is a different defect: no coverage is a silent bypass, no context is a fail-closed refusal of the whole command surface including `stop`. Names the pin whose inventory is derived from the tree, so the rule is enforced at the next bridge rather than restated.
The ROS 2 command gate moved to strands_robots/tools/_command_gate.py so that use_rtps and use_rosbridge consult the same blocklist as use_ros; a surface refused on one transport can no longer be sent on another. This branch added three DeepRacer surfaces to the blocklist and extended the two docs/security.md sentences describing it, so both sides edited the same text. Resolved by taking the shared owner in every case: - The three surfaces (/manual_drive, /vehicle_state, /enable_state) move to _command_gate.COMMAND_BLOCKLIST unchanged, so they are now gated on all three transports rather than only on in-process rclpy. Their justification moves with them, minus the bare-entry/final-segment rule the shared comment already states. - use_ros.py takes main's version wholesale. This branch's only edit to it was the blocklist and that comment, both of which now live in the shared module. - Both docs/security.md sentences keep main's widened subject (all three transports, one shared blocklist) and regain what this branch added to them: the Ackermann servo topic and arming services in the enumeration, and AckermannRosRobot beside RosBridgedRobot as a context forwarder. - tests/mesh/test_ackermann_command_gate.py and AGENTS.md rule 18 name the moved symbols: _DEFAULT_COMMAND_BLOCKLIST becomes COMMAND_BLOCKLIST, _is_command_blocked becomes command_block_message, and _COMMAND_ALLOW_ENV becomes COMMAND_ALLOW_ENV. docs/ros2-integration.md merged with no conflict and carries both the transport axis and this branch's surface table. Verified: the 12 suites covering the gate, the Ackermann bridge and both sibling transports report 545 passed, 1 skipped. Full tests/ run on the composition and on a pristine main worktree report an identical 627 failed / 981 errors, every one an absent optional dependency in this environment, with the branch-only failing-id set EMPTY and +68 passes that are this branch's own tests. ruff check and ruff format clean.
yinsong1986
left a comment
There was a problem hiding this comment.
Summary
Carries #1035 forward onto current main: a fourth mobile-base transport, AckermannRosRobot, presenting Ackermann-steering ROS 2 cars (stock AWS DeepRacer wiring in from_deepracer()) with the fleet-standard drive(linear, angular, duration) contract, a bicycle-model conversion to normalized ServoCtrlMsg servo pairs, a run-once init_services arming handshake, and a trailing zero-servo halt on every timed command. Both must-fix concerns from the earlier review round are resolved on this head: the trailing halt's verdict is captured from the finally and a drive whose halt fails now returns an error naming the possibly-live throttle and stop as the next action, and the bridge's command surfaces are gated - /manual_drive, /vehicle_state and /enable_state are default _command_gate.COMMAND_BLOCKLIST entries (so round 3's merge widens the gate to all three transports), drive/stop/enable thread tool_context into every use_ros call including the trailing halt, and the command tools are @tool(context=True) while get_scan stays contextless. The round-3 base merge correctly hands use_ros.py back to main's shared-gate refactor and moves the three entries and their justification to the new owner.
What's good
- The gate suite keeps the real
use_rosand doubles the rclpy transport - exactly the boundary the bridge suite patches over, which is why the original gap was invisible there - andTestEveryCommandingMeshBridgeHasAGateSuitederives the commanding-bridge inventory from the tree, so a fifth bridge is graded on arrival. - Validation goes through the shared numeric domains, and
tests/mesh/test_drive_command_numeric_domains.pyis extended to compare all four transports against each other, with the Ackermann wire contract (servo pair) pinned separately from the shared caller-facing domain. - The half-built-
reprand drive/stop-survey triage both land with parity entries in the whole-tree guards (test_repr_survives_partial_construction.py,test_bridge_stop_tool_parity.py), with the reasonAckermannRosRobotstays out of the Twist-assertingBRIDGEScases recorded next to the inventory. - Operator-facing posture is stated everywhere it is read: the blocklist table and final-segment examples in
docs/ros2-integration.md, the per-method gated-surface table, theSTRANDS_ROS2_COMMAND_ALLOWline,docs/security.md, and the example's SAFETY block, with both allowlist spellings pinned by tests. - All new/changed strings are plain ASCII; changelog fragment present and
assemble_changelog.py --checkpasses.
Verification suggestions
Beyond CI: pytest tests/mesh/test_ackermann_robot.py tests/mesh/test_ackermann_command_gate.py tests/mesh/test_bridge_stop_tool_parity.py tests/mesh/test_drive_command_numeric_domains.py tests/test_repr_survives_partial_construction.py (212 passed, 1 skipped on this head locally), plus tests/test_use_ros_command_blocklist.py tests/tools/test_use_ros.py tests/mesh/test_ros_bridge_command_gate.py (173 passed) to confirm the widened blocklist regresses nothing on the sibling transports.
…r sets with AckermannRosRobot main landed a fourth drive-owning mesh class (strands-labs#2689) and the operator-approval gate for every ROS 2 transport (strands-labs#2693) after this branch's last base merge. All four conflicts are set-membership: both sides added an entry to a list of the classes that own a drive contract, so each resolution is the union plus this branch's consolidation semantics. strands_robots/mesh/__init__.py both sides added exports tests/mesh/test_bridge_stop_tool_parity.py EXPECTED_DRIVE_STOP_CLASSES tests/mesh/test_drive_command_numeric_domains.py the drive-owner scan tests/test_repr_survives_partial_construction.py the surveyed classes AckermannRosRobot keeps its own drive/stop pair - it is not layered on MobileBaseRobot - so it joins the owning sets rather than the inheriting ones, and RosBridgedRobot/RtpsRobot stay asserted absent, which is what pins the consolidation instead of merely tolerating it. Measured on the composed tree, 14 affected files: 753 passed, 1 skipped, 0 failed. tests/mesh/test_mobile_base.py alone is 101 passed, including the gate-forwarding parametrization that was 2 failed before the tool_context fix.
Ackermann ROS 2 cars (AWS DeepRacer) as strands robots, brought current with
mainContinues @Vivek0712's #1035, whose commits are ancestors of this branch. That
PR is a complete, reviewed feature that has been unmergeable on a textual
conflict since 2026-08-04. This branch carries it forward:
mainmerged in,the conflict resolved, and the two defects that its stale base was hiding
fixed.
The feature
RosBridgedRobotcovers differential-drive bases (aTwistoncmd_velplusodometry). An Ackermann car exposes neither.
AckermannRosRobotkeeps the samedrive(linear, angular, duration)agent contract and converts through abicycle model to normalized servo commands (
ServoCtrlMsgon the DeepRacer),runs a declarative
init_serviceshandshake once before the first command,clamps speed, refuses over-long holds loudly, and always trails a timed command
with a zero servo message so a timed drive cannot leave the car driving. It
validates through the same
finite_number_error,positive_finite_number_errorandpositive_whole_number_errordomains thethree differential-drive bridges call, so the fourth transport refuses an
unusable velocity, hold or count with byte-identical text.
The conflict
One file,
docs/ros2-integration.md, where both sides appended a new section atthe same anchor: this branch a new
##"Ackermann robots (AWS DeepRacer)"section,
maina###"Safety-critical command surfaces need operatorapproval" subsection. Both are kept.
main's block goes first because it is anh3that continues the pre-existing## Safetysection - putting the newh2first would nest
main's subsection under "Ackermann robots". Resulting orderis
## Safety->### Safety-critical ...->## Ackermann robots ...->## Sim bridge ....Two defects the stale base hid
The branch's checks were green, and correctly so - both guards that fail it
landed on
mainafter its base (c1acf21d), and neither guard's file is onethe branch touches, so there is no file overlap to notice:
maintests/test_repr_survives_partial_construction.pyAckermannRosRobot.__repr__raises on a half-built instancetests/mesh/test_bridge_stop_tool_parity.py1.
__repr__on a half-built instance.__init__validatesnode_namethrough
_check_topicbefore assigning it, so a refused construction leaves theinstance the raising frame keeps alive rendering as
which sends the reader after
node_namewhen the real failure is theValueError: invalid node_name: 'bad name!'they already caused. Now wrapped intry/except AttributeErrordelegating tostrands_robots.utils.partial_construction_repr,exactly as
RosBridgedRobot,RosbridgeRobotandRtpsRobotdo, so the phrasea reader learns to recognise in a traceback has one owner. Half-built now renders
AckermannRosRobot(partially constructed, id=0x...). The class is triaged intoboth halves of that module's survey - the half-built sweep and the
documented-refusal cases - for parity with the other three bridges.
2. drive/stop survey inventory.
test_the_survey_finds_the_bridges_it_is_meant_to_coveris the non-vacuity half of the drive/stop guard: it pins that the scan finds the
bridges it is meant to cover, so a mis-rooted or empty survey cannot read as
compliant. A fourth drive/stop bridge has to be triaged there. The rule itself
already covered the class - the survey walks the mesh package - and renaming its
stop_tool failstest_every_bridge_owning_a_drive_stop_pair_builds_a_stop_tool.It stays out of
BRIDGESdeliberately, with the reason recorded next to theinventory: those cases assert a zero
geometry_msgs/msg/Twiston/cmd_vel, andan Ackermann car halts with a zero
ServoCtrlMsg(angle/throttle) on itsservo topic, which
tests/mesh/test_ackermann_robot.pyalready pins.Verification
Regression split, on the merged tree, over the two guard files:
The three are the two
reprcases plus the inventory case; the delta to 65 isthose three plus the two new parametrized cases the class now contributes.
Each fix is load-bearing, checked by mutation:
try/exceptfrom__repr__tolerant == len(EXPECTED_REPR_CLASSES))partial_construction_reprstop_tool tohalt_test_every_bridge_owning_a_drive_stop_pair_builds_a_stop_toolZero regressions, over 296 tree-walking guard files plus all of
tests/mesh,run identically on this branch and on a pristine
upstream/mainworktree:upstream/mainThe five failing ids are identical on both trees and all environment-bound
(four
DID NOT RAISE ImportErrorwhererclpyis importable, and oneauth_modecase sensitive to a mesh env var another test in the selectionsets). The +47 passed is this branch's own suite plus the two new cases.
ruff checkandruff format --checkclean overstrands_robots tests tests_integ.mypyover the same scope: 19 errors on this branch and 19 onpristine
main, with the branch-only difference empty.assemble_changelog.py --checkOK.No visual artifact: the diff adds a ROS 2 transport bridge and a
__repr__fallback, and changes no simulation, rendering or policy behaviour.
Supersedes #1035. Its changelog fragment is kept and extended with the
reprfix rather than replaced.
Round 2 changes
Two review concerns, both on safety paths in the changed code, one commit each,
plus the rule the second belongs to recorded where the next bridge will read it.
1. The trailing safety stop's verdict was discarded (
fcb5ed4).drivesentits trailing zero servo message from a
finallyand dropped the result, so whenthe main publish succeeded and the halt then failed - node or daemon dying
mid-hold - the caller saw the drive's
successfor a car still holding thecommanded throttle, on a tool whose description promises that a command with a
duration stops itself. The halt's result is now captured into a local from the
finally(so it still goes out when the main publish raises) and, when the drivesucceeded but the halt did not, returned as an error naming the published
command, the possibly-live throttle,
stopas the next action, and the halt'sown cause. A failed drive stays the reported cause; a latching single-shot sends
no halt, so there is no verdict to read.
Three cases in
tests/mesh/test_ackermann_robot.py, next totest_drive_trailing_zero_sent_even_when_publish_errorswhich covers the inversedirection: the failed-halt case, the both-failed precedence, and a control that a
single-shot success is not reinterpreted. Restoring the discard fails the first.
2. The bridge's commands were outside the
use_rosoperator gate (43ee3d5).Both halves were missing. The gate only fires on a blocklisted surface, and the
shipped DeepRacer wiring matched no entry (
/webserver_pkg/manual_drive, and the/ctrl_pkgmode services that arm the vehicle), so every servo publish andhandshake call went out with no prompt, no allowlist check and no audit row -
while the functionally identical
/motor_enablewas blocked. And nothing herecarried an operator context, so fixing coverage alone would have made every
command,
stopincluded, a fail-closed refusal./manual_drive,/vehicle_state,/enable_stateare default entries, spelled bare so the base-name rule covers the namespaced spellingsdrive,stop,enabletake atool_contextand thread it into everyuse_roscall (the trailing halt included);drive_<node>/stop_<node>are@tool(context=True)and forward the injected context, asRosBridgedRobotdoes.get_scanstays contextless -echois never gatedPinned by
tests/mesh/test_ackermann_command_gate.py(18 cases), which keeps thereal
use_rosand doubles the rclpy transport instead - the boundarytest_ackermann_robot.pypatches over, which is why neither half was visiblethere. It covers the prompt order over all three surfaces, a decline at the
handshake and at the servo topic, the halt reaching the operator, a timed drive's
trailing halt, both allowlist spellings, the headless refusal naming both
variables, and structural guards on
context=Trueand on every command callsite.
TestEveryCommandingMeshBridgeHasAGateSuitederives the mesh modules thatsend a command through
use_rosfrom the tree and compares them against aninventory of gate suites, so a fifth bridge is graded on arrival rather than
shipping ungated the way this one did.
context=Truefrom the drive tooltool_contextforward in_publish_servoDoc drift closed in the same commits, at every surface an operator reads to size
a pre-approval: the blocklist table and the final-path-segment example list in
docs/ros2-integration.md, a per-method gated-surface table plus theSTRANDS_ROS2_COMMAND_ALLOWline in its Ackermann section, the surfaceenumeration and the context-forwarding sentence in
docs/security.md, theexample's SAFETY block, and the class and module docstrings. The changelog
fragment carries both fixes.
3. The rule, recorded (
d44be59). AGENTS.md convention 18: a new bridgeships un-gated because the gate is keyed on surface names it does not know, and a
bridge suite that patches the module's
use_rossymbol cannot see it - so namingthe surfaces and threading the context are one change, either alone being a
different defect. It names the pin above rather than restating the rule, since
that pin's inventory is derived from the tree.
Round 2 verification. Same selection on this branch and on a pristine
mainworktree (tests/mesh,tests/tools/test_use_ros.py,tests/test_use_ros_command_blocklist.py,tests/test_repr_survives_partial_construction.py,tests/test_args_docstring_completeness.py,tests/test_docstring_xref_roles_resolve.py):mainThe 136 failing/erroring ids are byte-identical between the two trees and all
bound to optional dependencies absent from this runner (90
json5, 44awsiot,1
lerobot, and one pre-existingtest_iot_camera_ref_deliveryassertion). The+65 passed is this branch's new gate suite (18) plus its own earlier cases. The
968-test sweep of every guard that reads AGENTS.md passes (one
lerobot-boundfailure, present before the edit).
ruff checkandruff format --checkcleanover
strands_robots tests;mypyreports zero errors in either changed sourcefile, and its count on
use_ros.pyis 11 on both trees;assemble_changelog.py --checkOK.Round 3 changes
No review feedback outstanding; this round is a base merge forced by a sibling
change, plus the absorption its sequencing note called for.
mainlanded the ROS 2 command gate as a shared owner,strands_robots/tools/_command_gate.py, so thatuse_rtpsanduse_rosbridgeconsult the same blocklist as
use_ros- a surface refused on one transport canno longer be sent on another. Round 2 above added three DeepRacer surfaces to
that blocklist and extended the two
docs/security.mdsentences describing it,while it still lived in
use_ros.py. Both sides therefore edited the same text,and this branch went
CONFLICTINGthe moment that change merged.strands_robots/tools/use_ros.pydocs/security.md, both sentencesThe three surfaces move to
_command_gate.COMMAND_BLOCKLISTunchanged, whichwidens what round 2 achieved:
/manual_drive,/vehicle_stateand/enable_stateare now gated on all three transports rather than only onin-process rclpy. Their justification moves with them, minus the
bare-entry/final-segment rule the shared comment already states. The two
docs/security.mdsentences regain the Ackermann servo topic and armingservices in the enumeration, and
AckermannRosRobotbesideRosBridgedRobotasa context forwarder.
docs/ros2-integration.mdmerged with no conflict andcarries both the transport axis and this branch's surface table.
Three symbols this branch referenced moved with the gate, so its own suite and
the rule recorded in round 2 name them at the new owner:
use_ros._DEFAULT_COMMAND_BLOCKLIST_command_gate.COMMAND_BLOCKLISTuse_ros._is_command_blocked_command_gate.command_block_messageuse_ros._COMMAND_ALLOW_ENV_command_gate.COMMAND_ALLOW_ENVA tree-wide sweep confirms no reference to any of the old names survives, in
source, tests or AGENTS.md.
Round 3 verification. The 12 suites covering the gate, the Ackermann bridge
and both sibling transports: 545 passed, 1 skipped. The whole-tree graders
that read the edited docs and AGENTS.md: 111 passed.
Full
tests/on the composition and on a pristinemainworktree, sameinterpreter and same environment:
mainmainRead as a delta, per the rule that an absolute count in a partial environment
invents regressions: the failing-and-erroring id sets are compared directly and
the branch-only set is empty, with identical 627/981 totals on both trees.
Every one is an absent optional dependency here (
torch,mujoco,lerobot,rclpy,device_connect_edge,json5,awsiot). The +68 passed is thisbranch's own tests.
ruff checkandruff format --checkclean overstrands_robots tests(1469 files already formatted).