feat(booster-k1): Tier 1 obstacle-avoidance navigation -- MuJoCo + Webots sim-to-sim, real Zenoh bridge - #64
Open
Wanbogang wants to merge 39 commits into
Conversation
…er1) - robot.profile.yaml, skills.yaml, functions.yaml, payment-policy.yaml, execution-mapping.yaml for booster.k1.obstacle-nav-sim.v1 - simulator-agnostic DWA planner (simulation/common_policy/dwa_planner.py) shared by MuJoCo and (upcoming) Webots for honest sim-to-sim comparison - MuJoCo scene + runner: policy-driven navigation, real physics metrics (distance_to_goal, path_length, collision_count), verified success run Still missing before PR: Webots scene/controller, sim_to_sim_validate.py, Zenoh bridge with x402 payment gate, tests, docs/evidence.
- K1BaseProxy.proto + world file mirroring the MuJoCo scene (same goal, same obstacle positions, extern controller mode) - k1_navigation.py Webots controller importing the identical simulation/common_policy/dwa_planner.py used by the MuJoCo runner - Verified run: status=success, distance_to_goal=0.298m, path=5.36m, 0 collisions -- within ~1-2% of the MuJoCo result using the same policy code, confirming genuine sim-to-sim agreement - .gitignore for Webots auto-generated .wbproj cache files Still missing before PR: automated sim_to_sim_validate.py comparator, Zenoh bridge with x402 payment gate, tests, docs/evidence.
simulation/sim_to_sim_validate.py runs/compares MuJoCo vs Webots metrics.json against explicit tolerances (distance_to_goal abs 0.15m, path_length rel 15%, collision_count exact match, status exact match). Verified: PASSED -- distance_to_goal diff 0.0005m, path_length diff 1.3%, both simulators report status=success and 0 collisions on the identical goal=(5,0) obstacle-avoidance scenario.
bridge/action_validator.py validates every action envelope before it may reach the simulator: required fields, canonical paramsHash match, skill/network/asset/amount match the registry profile, and x402 payment must be verified + status=authorized + settled=false + not expired. 11 unit tests cover the fraud/error paths explicitly: tampered params hash, unverified payment, pending payment, already-settled payment (anti double-spend), expired authorization, wrong network/asset/amount, incomplete params. All 11 passing. Also adds examples/action-envelope.navigate-to-goal.json as the reference envelope shape.
bridge/replay_guard.py enforces the replayProtection policy from execution-mapping.yaml: idempotencyKey, actionId, and payment authorizationId are each unique -- reusing any one of them is detected and rejected as ReplayDetected before the simulator is ever dispatched (check_and_reserve happens pre-execution). 7 unit tests cover exact replay, cross-field replay (same actionId different idempotencyKey, same authorizationId different actionId -- the core anti-double-spend guarantee), legitimate independent actions, and result recording. All 7 passing.
…lator bridge/booster_k1_zenoh_bridge.py subscribes robot/tunnel/action, runs: validate_envelope -> replay_guard.check_and_reserve (BEFORE dispatch) -> dispatch_to_simulator (MuJoCo) -> publish terminal result on robot/tunnel/result, correlated by actionId per execution-mapping.yaml's correlationField. No fallback path exists: unreachable Zenoh, failed validation, or a detected replay all short-circuit before the simulator subprocess is ever invoked. Settlement gating is enforced by construction -- the published status is 'success' only when the simulator itself reports status=success; any other simulator outcome (collision, timeout, subprocess failure) yields status=error. 6 integration tests (simulator + Zenoh publisher mocked) cover the exact scenarios called out in prior review: replay must not cause a second simulator dispatch (asserted via mock call_count==1), and simulator failure/collision must not yield a success result. All 6 passing (24 total across the three test modules).
demo/send_test_action.py publishes a real action envelope over Zenoh
and waits for the correlated robot/tunnel/result, for manual
end-to-end demonstration without standing up the full Fabric/Tunnel
stack. 5s discovery delay before first publish avoids an early-publish
race between freshly-opened Zenoh sessions.
Verified against the real bridge + real MuJoCo simulator (not mocked),
raw session log captured in docs/evidence/terminal/bridge-e2e-session.log:
1. Valid paid action -> status=success, metrics matching the earlier
manual MuJoCo run (distance_to_goal=0.298m, path=5.43m, 0 collisions)
2. Unpaid action (verified=false) -> status=rejected,
errorCode=payment_not_verified, simulator NOT dispatched
3. Replayed actionId -> status=rejected, errorCode=replay_detected,
simulator NOT dispatched a second time
Condensed module and function docstrings to state the non-obvious
design decision in one or two sentences rather than restating what
the code already makes clear line-by-line. No functional change --
re-verified after each edit:
- action_validator: 11/11 tests passing
- replay_guard: 7/7 tests passing
- bridge: 6/6 tests passing
- dwa_planner: plan_step still produces the same output
- mujoco runner: re-run produces identical metrics (status=success,
distance_to_goal_m=0.2979)
- sim_to_sim_validate: still PASSED
- k1_navigation / send_test_action: syntax verified
tests/test_profile.py checks robot.profile.yaml, skills.yaml, execution-mapping.yaml, payment-policy.yaml, and functions.yaml agree with each other (profileId, skillId, topic names) and with what bridge/action_validator.py actually enforces in code -- catches drift between the documented contract and the real implementation. 12 tests passing. 36 total across all four test modules.
docs/README.md covers the bridge flow, repo layout, and exact steps to reproduce every result claimed in this submission: MuJoCo run, Webots run (including the extern-controller workaround needed for Webots' sandboxed snap Python, which cannot have packages installed into it), sim-to-sim validation, real end-to-end bridge run over Zenoh with all three scenarios (paid/unpaid/replay), and the test suite.
…evidence docs/task-traceability.md traces every booster-k1-tier-1 bounty requirement to concrete evidence in this repo, and separately addresses each concern raised in review of a related K1 submission (PR fabricfoundation#16/fabricfoundation#29): payment-verified runtime path, K1-specific evidence, complete registry profile, actionId-correlated terminal result, no unverified-payment fallback, replay must not cause a second action, and settlement gated on actual result success. Also states known limitations explicitly: geometric proxy K1 model, sim_time_sec divergence between engines, and single-instance replay guard storage.
…eal E2E run docs/validation-report.md consolidates every verified result in this submission: the 36-test suite breakdown, the real (not mocked) MuJoCo and Webots runs with their metrics, the sim-to-sim comparison table (PASSED, <2% divergence), and the three end-to-end bridge scenarios (paid/unpaid/replay) with a pointer to the raw session log. States limitations plainly: geometric proxy K1 model (no public Booster CAD exists to use instead), single-instance replay guard storage, and why the Webots leg of sim-to-sim needs a manual two-process step.
…dary docs/evidence/evidence-manifest.yaml documents the E2E bridge log (sha256-verified), the real MuJoCo run, and the real Webots run, each with an explicit trace of what was observed and what limitations apply. States plainly what is and is not claimed: the payment fields exercised are synthetic (constructed by demo/send_test_action.py to test the validation gate), not a real x402/Tunnel payment flow -- that logic is covered separately by tests/test_action_validator.py.
.github/workflows/booster-k1-obstacle-nav-sim.yml triggers on changes under registry/vendors/booster/k1/booster.k1.obstacle-nav-sim.v1/: runs the 36-test pytest suite, runs a real MuJoCo simulation and asserts status=success with zero collisions, then runs sim_to_sim_validate.py --skip-run against the MuJoCo output it just generated and the Webots baseline committed to the repo. Webots itself is not run in CI since it requires a GUI/extern- controller session (documented in docs/README.md) -- this is stated in the workflow file rather than silently skipped.
- dwa_planner.py: unused 'field' import from dataclasses - mujoco/runner.py: unused 'import time' - test_action_validator.py: unused 'timedelta' import - k1_navigation.py: unused 'k1_node' variable (only self_node is actually used for pose/velocity throughout the controller) pyflakes clean (exit 0) across all .py files after the fix. Re-verified: 36/36 tests passing, MuJoCo re-run produces identical metrics (status=success, distance_to_goal_m=0.2979).
…ontract Addresses the two most-cited review findings from prior tier-1 submissions (PR fabricfoundation#58, fabricfoundation#52): the action gate previously accepted any JSON body, including one with no registered action/skill, and dispatched it to Zenoh unconditionally. - handlers.go: PostAction now requires a JSON body with a non-empty 'action' field present on the ALLOWED_ACTIONS allowlist (comma- separated env var). Both an empty allowlist and an unlisted action are rejected (503/403) BEFORE PublishRobotAction is ever called -- fail-closed by default, not fail-open. - idempotency.go: new file-backed IdempotencyStore. Every accepted action reserves a durable pending record under a fresh actionId before publish; Reserve() called twice with the same actionId returns the original record and reports replay=true, so a retried or replayed request never re-dispatches. State persists to disk on every transition, so it survives a tunnel process restart. - PostAction now returns 202 {status, state, actionId, status_url} immediately instead of 200 with no correlation id. - New GET /action/:id/status (main.go route registration + handlers.go GetActionStatus) serves the durable status by the same actionId returned from PostAction. - handlers_test.go: the old TestPostAction_ValidJSON, which asserted {"command":"start"} (no action field) returns 200, is replaced -- it was asserting exactly the fail-open behavior under review. 10 tests total, covering the fail-closed paths, the async contract, and idempotency reservation/persistence/restart-survival. Not yet done: settlement (x402 /settle) is not yet deferred until after a succeeded terminal state -- tracked as the next change.
…terminal result Adds X402VerifyOnly middleware (verifies payment, never settles inline) and ExecutionWatcher (calls ProcessSettlement only after a terminal robot/tunnel/result confirms real execution success). Replaces the stock ginmw.X402Payment middleware, which settled synchronously as soon as PostAction returned -- wrong here since PostAction now returns 202 before the simulator has run.
PostAction now stores verified payment payload+requirements via Store.SetPaymentData instead of forwarding them inline in the published event. ExecutionWatcher reads it back for settlement.
…ution watcher payment_gate_test.go: a tampered/rejected payment yields exactly one verify call and zero downstream handler invocations, so an ActionEvent can never be published for an unverified payment. settlement_watcher_test.go: ProcessSettlement is called exactly once for a genuine terminal success, and exactly zero times for: non-success results, missing stored payment data, unknown actionIds, and replayed already-settled results. Also covers the settlement-failure path, which must leave the action unsettled rather than silently succeeding.
This was the missing link: payment_gate.go and settlement_watcher.go existed but were never connected to the running application -- main.go still used the old auto-settling ginmw.X402Payment middleware. - buildX402Server(): constructs the shared x402 resource server once, outside the WS reconnect loop (previously rebuilt on every reconnect for no reason). - setupRouter() now takes that server + a single shared *Handlers instance (so its IdempotencyStore is consistent across router rebuilds) and uses handlers.X402VerifyOnly instead of ginmw.X402Payment -- verify happens on every /action request, but settlement no longer happens automatically on any 2xx response. - New robot/tunnel/result Zenoh subscriber, declared once before the reconnect loop, feeds every result to a single ExecutionWatcher -- the only code path in this tunnel that calls ProcessSettlement. Build clean, go vet clean, gofmt clean, 21/21 tests passing (4 pre-existing + 17 new/updated across handlers, idempotency, payment_gate, and settlement_watcher).
…ecture
The tunnel changes (fix(tunnel) commits) moved payment verification and
settlement fully into Go: X402VerifyOnly gates every /action request
before an event is ever published, and ExecutionWatcher is the only
code path that settles, only after a terminal success result. The
published robot/tunnel/action event no longer carries any payment
fields -- just {actionId, action, params, timestamp}.
- bridge/common/zenoh_bridge/zenoh_bridge/action_event.py (shared,
used by unitree g1/go2/tron1 bridges too): added actionId to the
parsed event, matching the tunnel's new flat schema. Removed the
silent default of a missing action to 'stop' -- a missing action
is now a parse failure (returns None), consistent with fail-closed
handling elsewhere. Verified the three other consumers only read
.action/.params and already handle a None return by dropping the
event, so this is not a breaking change for them.
- booster_k1_zenoh_bridge.py: rewritten to parse via the shared
action_event.py (imported directly by file path to avoid pulling in
zenoh_bridge's ROS2-only command_mapper.py dependency) instead of
the old bridge-local envelope parsing. Removed the
action_validator.py payment-field validation entirely -- it is
provably dead code now: the tunnel never publishes payment fields
to actionTopic, so a bridge-side payment check can never see real
payment data. Replay protection (replay_guard.py) is retained,
keyed by the tunnel-issued actionId.
- action_validator.py and its 11 tests deleted (dead code).
- test_bridge.py rewritten for the new event schema; 8 tests, still
covering the scenarios that matter: wrong skill, missing params,
malformed/incomplete events, replay, simulator failure/collision.
- execution-mapping.yaml and payment-policy.yaml updated to describe
the actual envelope (no payment fields) and to point at the Go
files that now enforce each rule (enforcedBy fields), instead of
describing validation this profile's Python code no longer does.
- test_profile.py's payment-field-envelope test replaced with one
that checks the envelope actually matches the tunnel's schema.
27/27 tests passing (down from 38 -- the 11 removed were exercising
the now-deleted dead validator, not a regression).
…cilitator
cmd/e2e_test.go wires the real X402VerifyOnly gate and the real
ExecutionWatcher together (same code path as production main.go)
against a recordingFacilitator that never touches a real network --
analogous to the recording-facilitator pattern used in other tier-1
submissions' x402_harness.py.
Three tests:
- An unpaid POST /action is rejected 402 with zero facilitator
calls at all (verify or settle) -- the gate is not a rubber stamp.
- The core deferred-settlement proof: ExecutionWatcher.HandleResult
calls Settle exactly once, only for a genuine success result, only
after PostAction-time accept -- never at accept time itself. A
failure result never settles. A replayed success result for an
already-settled action never triggers a second Settle call.
- A facilitator-side settlement failure is recorded as
state=settlement_failed, settled=false -- never silently
upgraded to success.
Real EVM-signed PAYMENT-SIGNATURE construction is out of scope (would
require a private key + EIP-712 signing client not available in this
environment); the verify/settle separation itself is proven directly
against the same ExecutionWatcher and Store types production code
uses, which is the realistic unit boundary for this behavior.
Build clean, go vet clean, gofmt clean, all tests passing (24 across
cmd + internal + internal/handlers).
This script published a payment envelope directly to Zenoh, bypassing the tunnel entirely -- which was correct for the old architecture where the bridge validated payment fields itself, but is no longer representative: payment verification now happens exclusively in the Go tunnel via an HTTP POST /action request routed through a real Fabric proxy WebSocket connection, which this sandboxed environment does not have access to (same category of gap as not having Base Sepolia credentials). The equivalent proof now lives in tunnel/cmd/e2e_test.go, which exercises the real router/gate/watcher code path end-to-end against a recording facilitator -- see docs/README.md for how to run it.
Previous version described the bridge validating payment fields directly, referenced demo/send_test_action.py (now removed), and gave no instructions for the Go tunnel side at all. Now covers: the real flow (POST /action -> X402VerifyOnly -> PostAction -> Zenoh -> bridge -> result -> ExecutionWatcher settle), why the bridge no longer touches payment data, updated repo layout including the tunnel/ files, and full reproduction steps for the Go test suite (including the zenoh-c Cgo dependency setup) alongside the existing MuJoCo/Webots/Python instructions.
Previous version listed the old bridge-level payment checks (action_validator.py, now deleted) as the evidence. Replaced with traceability to the actual enforcement points: handlers.go's fail-closed allowlist, the async 202/status contract, the durable restart-surviving idempotency store, payment_gate.go's verify-only gate, and settlement_watcher.go's deferred settlement -- each pointing at the specific Go test(s) that prove it, plus the corresponding Python bridge tests where relevant. Limitations section updated to state plainly that no live Base Sepolia transaction or real EVM-signed payment exists in this submission, and why the verify/settle proof instead uses a recording facilitator against the production types directly.
…hitecture Test count breakdown updated (55 total: 27 Python + 28 Go, vs the old 36 Python-only figure). Added a payment-gate section summarizing the tunnel/cmd/e2e_test.go facilitator-call-count table (the core proof: zero calls for unpaid, exactly one settle for genuine success, zero for failure/replay/settlement-failure). Limitations section rewritten to state plainly that no live Base Sepolia transaction exists in this submission and why the recording- facilitator test is the appropriate substitute -- consistent with the task-traceability.md and README.md updates in the preceding commits.
…dge log historical New BK1-GO-E2E-001 entry documents tunnel/cmd/e2e_test.go as the current evidence for the payment-verification/deferred-settlement contract. BK1-E2E-001 (the old Zenoh-payload-payment demo log) is marked status: historical with an explicit note that it describes an architecture where the bridge itself validated payment fields -- which no longer exists (action_validator.py was deleted as dead code once the tunnel stopped publishing payment data to Zenoh). Retained rather than deleted because it remains accurate evidence of the bridge's dispatch-once/replay-rejection behavior, which is unchanged; the trace/limitations sections are corrected to say so explicitly rather than let the entry imply it still describes current payment enforcement. claimBoundary rewritten to state both what's proven (MuJoCo/Webots/ sim-to-sim, and the Go payment gate against a recording facilitator) and what isn't (no live Base Sepolia transaction, no real EVM-signed payment header).
Split into two jobs: python-simulation-tests (unchanged) and the new tunnel-payment-gate-tests, which installs the zenoh-c Cgo dependency, then runs go build/vet/gofmt-check/test across the whole tunnel module -- including internal/handlers (fail-closed gate, idempotency, deferred settlement) and cmd (the recording-facilitator end-to-end test). This was a gap noted in review of related tier-1 submissions: CI must exercise the real payment-gate code, not just the simulator. Trigger paths extended to tunnel/** and the shared bridge/common/zenoh_bridge/zenoh_bridge/action_event.py, since this profile's correctness now depends on both.
Added alongside CHAIN in .env.example: comma-separated allowlist of action/skill names, required for the fail-closed gate added in fix(tunnel): fail-closed action validation. Includes the Booster K1 example (k1_navigate_avoid_obstacles) as a concrete illustration without hardcoding any robot-specific value into this shared file.
tunnel/cmd/localserver/main.go reuses the exact production router wiring (X402VerifyOnly + PostAction + GetActionStatus + ExecutionWatcher) but listens on a real local TCP port instead of the Fabric WebSocket proxy, which this environment cannot reach. Ran a full live payment against the real x402.org facilitator and real Base Sepolia network: a wallet-signed EIP-3009 authorization (0xE7eB3Ff8...) triggered 402 -> signed payment -> 202 accepted -> real MuJoCo dispatch (status=success) -> robot/tunnel/result -> ExecutionWatcher settling only after that success -> real on-chain USDC transfer, verified independently via RPC: tx: 0xa2bfff89404f026f40f8c5782fd533ca9eeaa51017804aea4be467750443bf54 https://sepolia.basescan.org/tx/0xa2bfff8... status: SUCCESS, block 45462806 Full flow, terminal logs, and independent re-verification steps in docs/evidence/base-sepolia/live-payment-e2e.md. tunnel/.gitignore: exclude idempotency_store.json (localserver's runtime state file, not source).
…proof BK1-LIVE-001 documents the real wallet-signed, real-facilitator, real-on-chain-settlement run (see previous commit). claimBoundary updated to state this as claimed rather than notClaimed, and to narrow the remaining transport gap to just the Fabric WebSocket proxy layer itself (which tunnel/cmd/localserver substitutes with an identical router bound to a real local port).
…alidation report Both documents' limitations sections previously stated no live transaction existed; updated to point at the real one (tx 0xa2bfff89404f026f40f8c5782fd533ca9eeaa51017804aea4be467750443bf54) and narrow the remaining gap to just the Fabric WebSocket proxy transport layer, which this environment cannot reach regardless. validation-report.md gains a dedicated 'Live Base Sepolia payment' section with the full field table, alongside the existing recording-facilitator table (kept as fast deterministic CI coverage of the same verify/settle logic).
… duplication booster-k1-sim-01 was hardcoded 3 times inline while SKILL_ID was already a module constant -- inconsistent. 8/8 test_bridge.py tests still passing.
…cher return - Extracted testPaymentData() helper -- the same PaymentPayload/ PaymentRequirements JSON construction was duplicated verbatim across two tests. - buildTestRouter no longer returns an ExecutionWatcher the caller never uses (was assigned to _ = watcher in one test). - TestE2E_SettlementFailure_DoesNotMarkSettled now checks store.Reserve()'s replay return value, consistent with the other two tests instead of silently ignoring it. go build, go vet, gofmt, staticcheck all clean; 3/3 cmd tests still passing.
…scarding them Matches the error-handling style already used in cmd/main.go for the same three operations (zenoh session close, subscriber undeclare, HTTP server shutdown) -- previously these were silently ignored here. Build, vet, gofmt, staticcheck all clean.
…ead of discarding them Reserve() silently discarded persist() errors (_ = s.persist()) while UpdateResult() and SetPaymentData() both correctly returned theirs -- inconsistent, and meant a disk-write failure during reservation was invisible: the in-memory record existed but might never have hit disk, so a crash right after would silently lose replay protection for that action. Reserve() now returns (*ActionStatus, bool, error). PostAction fails closed (500 RESERVATION_PERSIST_FAILED) if the reservation could not be persisted, rather than proceeding as if it had. Updated all 14 call sites across handlers.go, handlers_test.go, settlement_watcher_test.go, and cmd/e2e_test.go. Build, vet, gofmt, staticcheck clean; all 24 tests passing.
…fail-closed Not a behavior change -- just documents the reasoning: if payment data never reaches the store, ExecutionWatcher finds MISSING_PAYMENT_DATA on the success result and marks settlement_failed rather than settling, so the fail-closed guarantee still holds at the one place that actually calls ProcessSettlement.
Never read in the function body -- pyflakes doesn't catch unused function parameters (only unused locals/imports), so this needed a manual pass. Verified: MuJoCo run still produces identical metrics (status=success, distance_to_goal_m=0.2979) after removal.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Simulation-only Booster K1 Tier 1 submission: policy-driven obstacle-avoidance navigation, triggered end-to-end from a paid RoboPay action gated by a fail-closed x402 payment flow in the Go tunnel, with MuJoCo + Webots sim-to-sim validation.
Major update since the initial submission: rebuilt the payment integration to match the architecture required by review of related tier-1 submissions (Boston Dynamics Spot #58, Reachy Mini #52) -- real fail-closed action gating, async accepted/pending contract, durable idempotency, verify-only payment gate with deferred settlement, and now a live Base Sepolia transaction proving the whole thing end-to-end with real money-equivalent value.
Full architecture, reproduction steps, and requirement traceability in
registry/vendors/booster/k1/booster.k1.obstacle-nav-sim.v1/docs/:docs/README.md-- architecture + how to reproduce every result belowdocs/task-traceability.md-- bounty requirements + prior-review concerns mapped to evidencedocs/validation-report.md-- full test/simulation/payment resultsdocs/evidence/-- sha256-verified logs, and live on-chain payment proofWhat's verified (not mocked, all reproducible)
Simulation:
status=success, distance_to_goal=0.298m, path=5.43m, 0 collisions -- policy-driven (DWA local planner), not a scripted trajectorydwa_planner.py), independent physics engine -- same resultsimulation/sim_to_sim_validate.py): PASSED, <2% divergencePayment gate (Go tunnel,
tunnel/internal/handlers):POST /action->202 {actionId, status_url}immediately;GET /action/:id/statusserves the durable terminal resultidempotency.go) -- survives a tunnel restart, replay never causes a second dispatch or a second settlementpayment_gate.go) -- payment is verified against the facilitator but never settled at accept timesettlement_watcher.go) -- the only code path that settles, and only after a terminalrobot/tunnel/resultwithstatus=successinternal/handlers+cmd) against a recording facilitator: zero facilitator calls for unpaid requests, exactly one settle call only after genuine success, zero additional settle calls for failure/replay/settlement-failureLive Base Sepolia proof (
docs/evidence/base-sepolia/live-payment-e2e.md):A real wallet-signed EIP-3009 payment, verified against the real
x402.orgfacilitator, gating a real MuJoCo dispatch, settled only after that dispatch reported success, with a real on-chain USDC transfer:0xa2bfff89404f026f40f8c5782fd533ca9eeaa51017804aea4be467750443bf54-- status SUCCESS, block 45462806, independently verifiable by anyone via any Base Sepolia RPCPython bridge (
bridge/booster_k1_zenoh_bridge.py): 27 tests -- payment verification now lives entirely in the Go tunnel, so the bridge trusts every event it receives and focuses on parsing, replay protection, and dispatching correctly.Notes for reviewers
docs/validation-report.md, not hidden.sim_time_secdiffers between MuJoCo and Webots (engine timestep resolution) -- spatial/outcome metrics agree closely and are what's actually compared for PASS/FAIL.tunnel/internal/client.go) itself is not exercised, since this environment cannot reach the Fabric proxy.tunnel/cmd/localserversubstitutes an identical router bound to a real local TCP port for the live Base Sepolia test above -- every line of payment-gate/settlement code exercised is byte-for-byte the same codetunnel/cmd/main.goruns in production; only the transport differs..github/workflows/booster-k1-obstacle-nav-sim.yml) now runs two jobs: the Python simulation suite, and a Go job that builds/vets/tests the tunnel package including the fail-closed gate and deferred-settlement logic.