Skip to content

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
fabricfoundation:booster-k1-tier-1from
Wanbogang:feature/booster-k1-sim-tier1
Open

feat(booster-k1): Tier 1 obstacle-avoidance navigation -- MuJoCo + Webots sim-to-sim, real Zenoh bridge#64
Wanbogang wants to merge 39 commits into
fabricfoundation:booster-k1-tier-1from
Wanbogang:feature/booster-k1-sim-tier1

Conversation

@Wanbogang

@Wanbogang Wanbogang commented Aug 1, 2026

Copy link
Copy Markdown

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 below
  • docs/task-traceability.md -- bounty requirements + prior-review concerns mapped to evidence
  • docs/validation-report.md -- full test/simulation/payment results
  • docs/evidence/ -- sha256-verified logs, and live on-chain payment proof

What's verified (not mocked, all reproducible)

Simulation:

  • MuJoCo run: status=success, distance_to_goal=0.298m, path=5.43m, 0 collisions -- policy-driven (DWA local planner), not a scripted trajectory
  • Webots run: same scenario, same policy code (shared dwa_planner.py), independent physics engine -- same result
  • Sim-to-sim validation (simulation/sim_to_sim_validate.py): PASSED, <2% divergence

Payment gate (Go tunnel, tunnel/internal/handlers):

  • Fail-closed action allowlist -- a request with no registered skill is rejected (503/403) before anything is published to Zenoh
  • Async contract: POST /action -> 202 {actionId, status_url} immediately; GET /action/:id/status serves the durable terminal result
  • Durable idempotency (idempotency.go) -- survives a tunnel restart, replay never causes a second dispatch or a second settlement
  • Verify-only gate (payment_gate.go) -- payment is verified against the facilitator but never settled at accept time
  • Deferred settlement (settlement_watcher.go) -- the only code path that settles, and only after a terminal robot/tunnel/result with status=success
  • 24 Go tests (internal/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-failure

Live Base Sepolia proof (docs/evidence/base-sepolia/live-payment-e2e.md):
A real wallet-signed EIP-3009 payment, verified against the real x402.org facilitator, gating a real MuJoCo dispatch, settled only after that dispatch reported success, with a real on-chain USDC transfer:

Python 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

  • The K1 base is a geometric proxy (cylinder + planar joints) in both simulators, not an official CAD model -- stated explicitly in docs/validation-report.md, not hidden.
  • sim_time_sec differs between MuJoCo and Webots (engine timestep resolution) -- spatial/outcome metrics agree closely and are what's actually compared for PASS/FAIL.
  • The production Fabric WebSocket proxy transport (tunnel/internal/client.go) itself is not exercised, since this environment cannot reach the Fabric proxy. tunnel/cmd/localserver substitutes 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 code tunnel/cmd/main.go runs in production; only the transport differs.
  • CI (.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.

Wanbogang added 16 commits July 31, 2026 16:49
…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).
@Wanbogang
Wanbogang requested a review from a team August 1, 2026 11:42
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant