Add robot profile: boston-dynamics/atlas/boston-dynamics.atlas.mujoco-pybullet-webots-shelf-inspection.v1 - #120
Conversation
4f6b679 to
e5278f0
Compare
39a21af to
c63126c
Compare
e2ac063 to
4a81c1b
Compare
…r 1) Payment-gated, policy-driven multi-target shelf inspection on a free-standing Boston Dynamics Atlas v4, executed over the tunnel transport and validated across MuJoCo, PyBullet and Webots R2025a from one pinned robot description. Payment path demo_go_tunnel.py drives this repository's own Go tunnel — the binary that mounts the upstream x402 gin middleware and a real facilitator client — behind a minimal stand-in for the hosted Fabric backend. The tunnel refuses an unpaid action with 402 and advertises payment requirements, and refuses a forged authorization with 400 after consulting the live facilitator; neither reaches Zenoh or the simulator. TUNNEL_BUILD.md records how the binary was built. The accepting side needs an EIP-3009 authorization signed by a funded wallet and is deliberately not claimed. demo_tunnel.py proves the Zenoh path itself with a Python client: a verified action is published on robot/tunnel/action, executed, and answered on robot/tunnel/result carrying the originating action_id, and settlement only follows a correlated success. A paid action actuates the robot once. The idempotency claim is taken atomically and persisted, so concurrent retries and restarts both collapse to a single actuation; a repeat with different parameters or a different payment is refused. Identity the tunnel correlates on — action_id, robot_id, skill_id, idempotency_key — is mandatory rather than defaulted. One price (0.001 USDC = 1000 raw) and one USDC address are declared in task.py and asserted against the registry, the demos and the settlement layer. Robot Atlas v4 is fetched, never vendored: models/model.lock.json pins openai/roboschool@d32bcb2 (MIT). Collision geometry upstream is analytic, so no mesh assets are needed and none are committed. MuJoCo imports the URDF as MJCF, PyBullet loads it directly, and webots_env.py converts it to a PROTO. Controller STAND -> REACH/VERIFY per target -> RETURN, with a damped least-squares resolved-rate loop re-solved from the measured end-effector pose every control tick. No recorded trajectory. The Jacobian and the gravity feedforward are derived from the URDF rather than any engine, so all three backends run the same maths; tests check them against MuJoCo's own. Measured MuJoCo 3/3 targets, 9.54 mm mean error, min pelvis 0.9084 m, 0 collisions PyBullet 3/3 targets, 12.18 mm mean error, min pelvis 0.9395 m, 0 collisions Webots 3/3 targets, 8.98 mm mean error, min pelvis 0.8982 m, 0 collisions Cross-engine mean-error spread 3.20 mm; sim-to-sim verdict PASS. Fall threshold is 0.70 m of standing height, not floor contact. Repeated MuJoCo runs hash identically, and a clean clone reproduces them. 111 tests cover the tunnel contract, the facilitator, idempotency, the payment gate, the model integrity, the kinematics and the task.
4a81c1b to
1568e4b
Compare
The reported end-effector speed was read from `data.cvel[hand][:3]`. MuJoCo lays `cvel` out as `[angular; linear]`, so the figure published as 5.76 m/s was the hand's angular rate in rad/s. Measured against the hand's own displacement it moves at 1.13 m/s, and it never exceeds 1.5 m/s. All three engines now measure the same quantity — how far the hand travelled in one control step — and a test checks the reported number against that displacement, since no task assertion fails when this metric is wrong. The episode peak turned out to be a RETURN artefact: RETURN assigns the stance pose straight into the joint targets, bypassing the rate limit that shapes REACH, so only the actuator limits bound the retraction. The speed reached while actually inspecting is therefore reported separately, and the remaining spread between engines (0.34 / 0.37 / 2.82 m/s) is servo stiffness against a shared joint-space ceiling of 0.01 rad per 2 ms control step. Also stops calling an action "paid" where only the payment was validated; "paid" now appears solely where value actually settled on-chain. Evidence regenerated on all three engines, Webots included, and every checksum in the manifest re-verified.
demo-e2e-evidence.json recorded the accepted demo step as status SETTLED with a tx_hash and block_number 0, and tunnel-e2e-evidence.json recorded it as settled: true while stating payment_verification: protocol_checks_only a few lines further down. Both demos run in-process with no wallet. A reviewer reading the PR body would have understood the boundary; a reviewer opening either artifact on its own would have read it as proof that USDC moved. The fix is in the ledger rather than in the wording, so the claim cannot come back: SETTLED now requires a settlement transaction hash and the block that contains it. Without one the entry becomes SETTLEMENT_ELIGIBLE — the execution succeeded and the policy authorises payment, but nothing moved — and settlement_tx_hash is null. The receipt a caller presents and the hash of a settlement transaction were sharing one field; they are different things, and conflating them is what produced the misleading artifact. The relay reports settlement_eligible instead of settled for the same reason. Tests updated to lock the honest semantics: a receipt alone can no longer earn SETTLED, and an eligible entry must carry no block number.
Every other demo here proves the refusing side — unpaid refused, forged authorization refused by the live facilitator, replay refused — because that half needs no money. This is the accepting side: a real EIP-3009 authorization verified by the live facilitator, executed on the robot, then settled in USDC on Base Sepolia and read back from a public RPC. Two things make the artifact mean something rather than merely look impressive. The authorization is bound to the action. EIP-3009 lets the signer choose the 32-byte nonce, so this sets it to keccak256(action_id). The same value appears in the AuthorizationUsed event the token emits, which means a reviewer can recompute it from the action id alone and confirm this settlement paid for this action. Nothing else in an x402 receipt ties a payment to the work it bought. Settlement happens after execution, never before: verify, run, and only then settle, so a failed episode leaves the authorization signed but unspent. The signing key is read from SETTLEMENT_PRIVATE_KEY or SETTLEMENT_MNEMONIC and is never printed, logged, or written to any file. Accepting the recovery phrase directly removes the conversion step, which is where keys tend to end up pasted somewhere they should not be. --payer refuses to run if the configured key signs for a different address.
Closes the last gap in the evidence: until now every payment result here was either a refusal or a protocol-level acceptance with a synthetic receipt. This is one action carried the whole way — an EIP-3009 authorization signed by a funded wallet, accepted by the live x402 facilitator, executed on the robot, and settled only after the episode reported every target reached. action act-paid-de66513f791b verify isValid: true, from the live facilitator robot success, 3/3 targets, 0 contacts, correlated by action_id settle 0x2b3b71d0ce18554a4927e1145a704359bad35c209f632dc414926b995aac0f39 chain block 45706216, 0.001 USDC, the price the profile declares The settlement is tied to the action rather than merely adjacent to it. EIP-3009 lets the signer choose the authorization nonce, so this run sets it to keccak256(action_id); the token emits that nonce in AuthorizationUsed, so anyone can recompute it from the action id and confirm this transfer paid for this action. A receipt and an execution are otherwise two unrelated facts. The transaction was submitted by the facilitator's own address — the one its /supported endpoint advertises — which is independent evidence the payment went through the live facilitator, and why the payer holds no ETH: under EIP-3009 the payer signs and the facilitator pays the gas. Also removes settlement.py. Its OnChainSettlement was unused and could not have worked: the transferWithAuthorization ABI omitted v, r and s, so the call it built had six arguments where USDC's function takes nine. Leaving code that claims to settle but cannot is worse than not having it.
The bounty's end-to-end flow runs client -> Fabric relay -> tunnel -> Zenoh -> simulator -> result -> relay. This tunnel could only do the middle of that: it published paid actions onto Zenoh, but a caller had no way to ask what robot was on the other end, what it could do, what that cost, or how a submitted action ended. demo_go_tunnel.py papered over the gap with a local stand-in for the hosted backend, which is exactly the substitution worth removing. Adds three read-only endpoints and nothing else. POST /action is untouched. GET /robot what robot is connected GET /skills what it can do and what each skill costs GET /action/:action_id/status how a submitted action ended Discovery answers from the profile's own skill-catalog.json, so a quoted price cannot drift from the price the registry publishes. Status is not synthesised: the tunnel subscribes to the same robot/tunnel/result topic the simulator publishes on and stores what arrives, keyed by action_id. An action nobody has answered for reads as pending, and a failed one reads as failed — reporting anything else would make the endpoint a decoration rather than a status. demo_fabric_e2e.py drives the real hosted relay at api.fabric.foundation with this tunnel dialled out to it over WSS. Confirmed working: the relay reports the robot connected, returns both skills at the catalogue price, and refuses an unpaid action with 402 carrying real payment requirements. Two details found by running it rather than by reading about it. x402 v2 matches a payment against the advertised requirements on scheme, network, amount, asset and payTo, all read from an "accepted" object on the payload — the v1 shape of scheme and network at the top level matches nothing and is refused with "No matching payment requirements". And v2 names the price "amount" where v1 called it "maxAmountRequired". Note on ordering: the tunnel's middleware settles as part of accepting a payment, so on this path settlement precedes execution. real_paid_run.py settles after the episode succeeds. Both are real; they are not the same guarantee, and the artifacts say which is which. The handlers package test fails in this Windows environment with a DLL-loading error, identically on the unmodified code; it is not caused by this change.
fabric-relay-e2e.json records one action carried end to end with nothing stood
in for: the hosted relay at api.fabric.foundation, this repository's Go tunnel
dialled out to it over WSS, the live facilitator, Zenoh, MuJoCo, and a real
settlement. Until now the relay was substituted by a local proxy and the paid
path reached the robot over Zenoh directly.
discovery GET /robots/{id}/skills -> inspect_shelf, stop @ 0.001 USDC
unpaid 402 from the relay, carrying payment requirements
paid accepted
execution 3/3 targets
status succeeded, correlated by action_id, read back from the relay
settlement 0x34d34a9d931c92f32ad0e993fc5c72bf730cdadea6826ebf0de821fe41ed4cc5
block 45707426, 0.001 USDC, nonce = keccak256(action_id)
The price is discovered rather than assumed: the payment is built from the
amount the relay quotes in its 402, and the run asserts that amount matches the
price the catalogue publishes. A profile whose advertised price had drifted from
what its tunnel charges would fail that check instead of passing quietly.
One difference between the two paid paths is recorded rather than smoothed over.
On the relay path the tunnel's middleware settles as part of accepting the
payment, so settlement precedes execution; real_paid_run.py settles only after
the episode reports every target reached. Both are real and both are bound to
their action_id, but they are not the same guarantee.
fabric-relay-failure.json sends a paid action whose maxDurationSec is below the bound the catalogue declares. Two separate things come out of it and they are reported separately, because merging them would flatter the result. Failure semantics hold. The bridge refuses the action with INVALID_DURATION and the refusal reaches the caller through the relay's own status endpoint as state: failed, carrying the real error code rather than a generic one, still correlated by action_id. Nothing was smoothed into a success. Payment safety does not hold on this path. The action was refused and it was still paid for — 0.001 USDC in 0xdb69d2d408682202fffd37a11e836cbf71c64e3869e5f9814f33498f082a0aec, block 45707813. The tunnel's x402 middleware settles as part of accepting a payment, before the robot is reached, so a refusal downstream comes too late. That is a property of the middleware and not of this bridge: real_paid_run.py asks the facilitator to settle only after the episode reports every target reached. The profile now contains both orderings and the artifacts say which is which, rather than presenting the safer one as though it were universal. The finding is recorded in the artifact itself under payment_safety, in the validation report, and in the PR's known boundaries. On why the failure is a refused parameter rather than a timeout: the catalogue declares maxDurationSec minimum 5, and at 5 seconds the episode completes all three targets — measured over three runs, not assumed. There is no in-bounds duration that produces an execution timeout, which is a property of a well-chosen bound rather than a gap. Also fixes the invariant that reported this run as failing. It compared targets_completed against targets_total, which are both absent when an action is refused before execution, so equal-and-missing read as "every target reached". The checks now assert what actually matters: the state is failed, the reason is the real one, and no successful execution is claimed.
Section 9.1 reported the status timestamp as 23:58:30Z. The artifact says 23:58:31Z. One second, and nothing depends on it — but it was substituted rather than read, which is the one thing an evidence document cannot afford. Now taken from the artifact, in full ISO form, alongside the action_id so the report, the manifest and the PR body all name the same run. The section also read as a single result. It is two, and only one of them passes: execution failure semantics pass — the refusal reaches the caller as failed, with its real reason, correlated by action_id — while post-execution settlement semantics fail on the hosted relay path, because the middleware settles before the outcome is known. Stated that way rather than averaged into one verdict, and no compliance with post-execution settlement is claimed for that path.
The previous revision settled on acceptance: the x402 middleware charged the
payer as soon as the payment was verified, before the robot had run, so an
action the bridge then refused was still paid for. That was measured rather
than suspected, and it is the one thing the payment policy must not do.
The middleware already had the hook. It settles after the route handler returns
and skips settlement when the response is an error, which means the status code
POST /action chooses *is* the settlement decision. So the handler now waits for
the robot's own answer over Zenoh before replying:
200 every target reached -> settle
502 the robot reported a failure -> do not settle
504 the robot never answered -> do not settle
400 no action_id to correlate on -> do not settle
An outcome that cannot be correlated cannot be known, so it cannot be paid for
either. No middleware change and no new endpoint; POST /action keeps its route
and its request shape.
Proven twice over. Six tests in handlers_test.go hold the contract with no
wallet and no chain, by injecting the transport. And through the hosted relay
with a real wallet:
succeeded atlas-inspect-1787193393
0x2eca1865602dc880224ab762be20f93ba0b0c81e4bd26654445bca9d3868cd63
block 45712565, 0.001 USDC, authorizationState = true
refused atlas-inspect-1787193428
HTTP 502, no transaction, authorizationState = false
That last field is the point. "We recorded no transaction hash" is an absence of
evidence and proves nothing about whether the payer was charged. EIP-3009 tokens
keep their own map of spent authorization nonces, and because the nonce is
keccak256(action_id) a reviewer can recompute it from the action id and ask USDC
directly whether that action was ever paid for.
One subtlety worth recording: the query is pinned to the block containing the
settlement and asked only after the chain head passes it. A public endpoint
serves a receipt before it has applied the block's state, and answering from
that window reports a spent authorization as unspent — wrong in the direction
that would flatter this profile.
Sections 9 and 9.1 of the validation report and the manifest's settlement
summary are rewritten around the corrected behaviour; the stale claim in
section 7 that the accepting side of facilitator verification is not proven is
gone, replaced by a table saying which artifact proves what.
The entry for fabric-relay-failure.json said the refused action was "settled anyway, because this path settles on acceptance". That stopped being true in the previous commit, and a manifest is exactly the file a reviewer trusts to describe what an artifact contains.
Four corrections, none of them to the robot.
**A refused action reached the robot.** PostAction registered a waiter, published
to Zenoh, and only then noticed the request carried no action_id — so a request
that was about to be refused had already gone out on the wire. The validation now
happens before anything is published. The test that was supposed to cover this
only asserted the status code was >= 400, which passes just as happily when the
action has already been sent; it now asserts nothing was published, and a table
test covers the other refusal paths. Reintroducing the old ordering makes both
fail, which is the check that they are worth having.
**The published contract disagreed with the code.** functions.yaml promised 202
accepted/pending; the route answers 200, 502, 504 or 400. The contract now says
what runs, including why it is synchronous: the x402 middleware settles as soon
as this route answers without an error, so a 202 charges the payer before the
robot has run and a failed episode is still paid for. That was measured on this
branch, not assumed. Deferring settlement past the middleware is not available
to a route behind it, so the route waits instead — and the asynchronous read is
still there as GET /action/{action_id}/status, which reports pending until the
result arrives. The declared responses for /robot, /skills and the status route
now match their handlers too.
**A clean checkout could not run the paid path.** real_paid_run.py and
demo_fabric_e2e.py import eth_account and eth_utils, which were in no
requirements file. Added, with a note on what they are for.
**CI re-verified the wrong transaction.** The step called "Re-verify the on-chain
settlement" checked a 1.0 USDC transfer that predated the paid run and was bound
to no action, while the profile's real settlement was 0.001 USDC. It now reads
the settlement out of real-paid-run.json and fails unless the transaction
succeeded *and* its AuthorizationUsed nonce equals keccak256(action_id) — a
transfer of the right size that is not bound to the action proves the asset
moved, not that this action was the reason.
Also fixes a lifecycle bug: the result subscription is declared once per process
while setupRouter builds a fresh Handlers on every config restart, so the new
handler had been reading an empty store and would have reported every action
pending for ever. The store is now shared with the subscription; tests override
it for isolation.
The success criteria ask for two things that the stock x402 middleware cannot
both give: an immediate accepted/pending response, and no settlement when the
action fails. That middleware settles as soon as a protected route answers
anything under 400, so a 202 charges the payer before the robot has run. The
previous commit chose payment safety and answered 200/502/504 after waiting,
which met the second requirement by breaking the first — and left the published
contract describing a 202 the code never sent.
Both now hold. A gate replaces the stock middleware and keeps its 402/verify
half unchanged: an unpaid request still gets 402 with the advertised
requirements, and a payment the live facilitator rejects still never reaches the
robot. Instead of settling, it hands a settlement callback to the handler.
POST /action publishes, answers 202 with the action_id and a status URL, and a
background watcher invokes that callback only when the correlated result reports
success. A failure or a silent robot leaves the authorization signed and unspent.
Both halves are readable from GET /action/{action_id}/status, which now carries
the settlement record alongside the execution state.
Verified end to end through the hosted relay, on chain both ways:
succeeded atlas-inspect-1787197727 — 202 immediately, then
0xfd9eda75ddc6c6f979eb2571e6e85ef3a6f50d670f3f8ad252107723e21e6940
block 45714728, 0.001 USDC, authorizationState = true
refused atlas-inspect-1787197752 — 202 immediately, no transaction,
authorizationState = false
Eight tests hold the contract without a wallet: 202 carries the action_id,
success settles exactly once, failure and timeout settle zero times, and a
request with no action_id is refused before anything reaches Zenoh.
Also corrects three things the contract claimed and the code did not.
functions.yaml never declared skill_id, which the bridge requires and refuses to
infer. skill-contract.test.yaml still asserted a 1.0 USDC settlement — the price
has been 0.001 everywhere else for some time — and described the Zenoh
walkthrough as settled when that demo holds no wallet and settles nothing.
Four surfaces still described earlier revisions of this profile, and a reviewer reading any one of them would have found it contradicting another. skill-contract.test.yaml asserted that a valid request answers 200 and settles. Both halves were stale in different ways: 200 is right for the in-process relay, which is what that case actually covers, but that relay holds no wallet and settles nothing; and the hosted path answers 202. The case now says which path it describes and reports settlement as eligible, and three cases are added for the hosted contract — accepted immediately, settles only after success, and never settles a failure — each pointing at the Go test that holds it. The bridge README's payment table read as though the in-process relay settled, and cited a 1.0 USDC transfer as this skill's settlement. It now separates the in-process table from the two paid paths and cites the 0.001 USDC settlement bound to its action_id. evidence-manifest.yaml said the tunnel answered 502 for the refused action, two lines above recording 202. The 502 was true of the previous revision. It also carried the unrelated 1.0 USDC transfer, which invited the question of whether the price is 0.001 or 1.0; it is 0.001 everywhere, and a transfer bound to no action is not evidence for this skill, so it is gone from the reviewer-facing manifest and from section 8.2. settlement_evidence.py verified the binding but only reported the rest. It now fails on a mismatched amount, asset, payer or payee as well, with the expected values read from the profile and the paid run rather than restated — so a price changed in one place cannot leave this check agreeing with a stale copy of itself. Confirmed by asking it to expect a different price and watching it refuse the real transaction.
It said a payment the facilitator will accept 'is not claimed here'. True of that demo, which has no wallet and stands in for the relay with a local proxy — but read as a statement about the profile it contradicts two artifacts that settle real USDC. It now says which demo proves what.
Two gaps, both about what a reviewer can check rather than take on trust. The workflow exercised the Python bridge, the Zenoh walkthrough, the live facilitator and the settlement, but never built the Go tunnel — so "the tunnel in this PR builds from a clean checkout" was a claim with nothing behind it. A job now fetches zenoh-c, builds the binary, vets it, and runs the handler tests that hold the payment contract: a 202 says nothing about the outcome, success settles exactly once, failure and timeout settle zero times, and a request that cannot be correlated never reaches Zenoh. All without a wallet or a chain. The second is a boundary rather than a fix. The payee is configured and the settlements did go to it, but that is configuration matching, not a cryptographic binding between the robot's identity and its wallet — that handshake lives in the shared upstream tunnel and gateway, and this profile consumes the identity it is given. Section 9.2 says so plainly, and distinguishes it from the binding this profile does prove: the settlement to the action, via keccak256(action_id) recorded in the token's AuthorizationUsed event.
The profile has a GIF of the robot and JSON artifacts for the payment, and a reviewer has to hold the two side by side and take on trust that they describe the same run. This records them together: a terminal pane that fills in as the real HTTP exchange happens — discovery, the 402 with its quoted amount, the signed authorization, the 202, the correlated result, the settlement and the BaseScan link — beside the simulator rendering the episode that exchange paid for. It is one pass over one action, not two runs edited together. The bridge is given a rendering executor, so the frames are of the episode the paid action triggered; rendering a second, unpaid episode afterwards would look identical and prove nothing about the first. AtlasZenohBridge takes that executor as a parameter, which is the only change outside the new module. --dry-run records discovery and the 402 only, signs nothing and spends nothing, which is enough to check the layout.
atlas-paid-action.gif is a single pass over one action: the terminal pane fills
in with the real exchange — discovery, the 402 and its quoted amount, the signed
authorization, the 202, the correlated result, the settlement and the BaseScan
link — beside the simulator rendering the episode that payment bought. The
existing atlas-shelf-inspection.gif stays; it is the close-up of the task, and
this one is the whole flow.
action atlas-inspect-1787200740
settlement 0xea4353bf089b08c1758bba6a27e4faa45503c0536a2a8b921d9e7de94486abb8
block 45716247, 0.001 USDC
nonce 0x24bfe0a79140d5b24e4b8df20a648e3c1bba3ad5db42e30b5ae6b2de50a44ae5
That transaction was located on chain by its nonce rather than by its hash:
keccak256(action_id) is enough to find the AuthorizationUsed event, which is the
binding working in the direction a reviewer would actually use it — start from
the action, arrive at the payment.
Also widens the log pane's line budget. At the previous spacing the relay URL
had scrolled off by the time the settlement arrived, which is the one moment a
reviewer is most likely to pause on.
…inding The tunnel checked only action_id before publishing. The simulator bridge requires four identity fields and refuses an envelope without them, so a request missing robot_id, skill_id or idempotency_key was put on Zenoh and rejected at the far end — an invalid request reaching the transport at all. All four are now checked before anything is published, and the table test covers each one missing, asserting the publisher stayed untouched. On identity and payee binding, section 9.2 now says which half this profile can speak for. The identity the tunnel answers for and the address it is paid to come from one configuration and are advertised together, the 402 quotes that same payee, the x402 matcher refuses a payment whose payTo differs, and the settlements landed there — two tests hold the advertising half. The authenticating handshake that would make the identity unforgeable rather than declared belongs to the shared tunnel and gateway; this profile consumes it and does not claim it. A robot profile is not the layer that can. The README gains what the criteria ask of it and did not have: wallet setup (including that no ETH is needed, because under EIP-3009 the payer only signs), the testnet configuration in one table, and both key variables. Its Zenoh line also said a reachable router was required while the walkthrough two pages up ran in peer mode without one.
…tion Section 9.2 said the authenticating handshake was upstream and unproven, and left it there. Reading the tunnel rather than reasoning about it gives a more exact answer, and a better one. There is no package called robotsdk in this repository. The robot-side outbound client it ships is tunnel/, which depends on github.com/unibaseio/aip-go-sdk and dials the relay over WSS — the same component the merged Tier-1 profiles use, and the one this profile uses unmodified apart from three read-only endpoints. The handshake is in it. With AIP_ENABLED=true, cmd/main.go runs aipauth.EnsureAuth, which returns a bearer token and a wallet address, and internal/aipagent registers the agent with Handle set to the robot id and UserID set to that wallet. That is identity bound to wallet, performed by the shared tunnel through the SDK. This profile does not bypass it; the recordings run with it disabled because EnsureAuth drives an interactive browser authorization flow that an unattended, reproducible demo cannot perform. So the honest split is: implemented in the component this profile uses, not exercised in these recordings, not reimplemented here — and separately, the payee half, which this profile does hold and test, is unchanged.
|
Hi @Junzhe 👋 Atlas Tier 1 is ready for review. Implementation, simulator evidence, sim-to-sim validation, the paid action flow, the Go tunnel, CI and the reproducibility docs are all complete, and I'd really appreciate a look when you have a chance. Happy to address any feedback. One clarification I'd rather ask than assume — the The Tier 1 criteria say the bridge connects outbound to the Fabric relay using
Could you confirm whether this repository's Separately, on identity: the tunnel does contain the authenticated registration path ( Thanks for taking the time 🙏 Edited to correct one detail: an earlier version of this comment said the outbound client depends on |
None of these change behaviour; all four described a revision this profile no longer has, and a reviewer opening any one of them would have found it contradicting the code. The profile README still showed the paid case as `200 … settled`. That was the synchronous revision. The route answers `202`, the outcome arrives on the status endpoint, and settlement follows the result — the table now says that, including that a replay surfaces in the correlated result rather than as an HTTP code, because the tunnel has already answered by then. functions.yaml declared 403, 409, 429 and 503 for POST /action. The route produces none of them: it answers 202, 400, 500 or 502, and 402 comes from the payment gate. Declaring codes nothing emits invites a reviewer to look for behaviour that is not there, so the list is now exactly what the handler can return, with a note on where the outcomes the robot decides actually appear. skill-contract.test.yaml listed three mandatory identity fields where the bridge and the tunnel require four — robot_id was missing. Added, along with a case for the tunnel-side refusal that keeps an incomplete identity off Zenoh entirely. And a comment in demo_fabric_e2e.py still said POST /action blocks until the robot answers. It has not since the 202 contract landed; the timeout it explains is for the status polling that follows.
The contract's header claimed every expectation below it is enforced by an executable test under bridge/boston_dynamics/atlas_bridge/tests. Most are — 27 of 36 — but four are held by Go tests under tunnel/internal/handlers and five by demos and verification scripts that exit non-zero when their invariants fail. The header now says that, and points at the enforcedBy field on each case, so a reviewer checking one is not surprised to find it somewhere other than where the header promised.
sign_for() fell back to the compiled-in price, payee, asset and USDC metadata whenever the 402 omitted them. Nothing exercised that path — the relay quotes all of it — but the run exists to show the payment is built from what the robot asked for, and a fallback means it could have passed on a number the relay never sent. Every field is now taken from the 402 and a missing one is fatal, which is checked by dropping each field in turn and watching it refuse. Three contract corrections alongside it, all cases of a document describing a route that answers differently. skill-contract.test.yaml expected 409 for a replayed receipt. That is the in-process relay's answer and it is correct there, but the case did not say so, and on the hosted path the tunnel has answered 202 long before the bridge recognises a duplicate. The case now names its path, and a second case covers the hosted one: 202, no second actuation, and the refusal arriving as the state and error_code of the status endpoint. functions.yaml declared only 200 for GET /skills, while the handler answers 503 when the catalogue is unreadable — it says so rather than serving an empty catalogue, and now the contract says so too. A comment in demo_fabric_e2e.py still described the middleware settling on acceptance. It has not since the deferred gate landed; the header it explains is now only useful on the rejecting path.
The contract described 402 as covering refused payments and left 400 to body problems, which does not match what the gate does. Reading it rather than inferring: `extractPaymentV2` failing returns 400 with "Invalid payment", and every other refusal — no payment, no matching requirement, a facilitator that says no — comes back as 402 with the requirements attached. The line between them is whether the payment could be read at all, not how badly it failed. The contract now says that, and it matches the recorded evidence: the unpaid action in go-tunnel-e2e-evidence.json is 402, the forged one is 400. Worth noting because the obvious tidy-up would have been wrong. Narrowing 402 to "payment missing" and moving refusals to 400 reads sensibly and contradicts both the library and a live run here, where an unfunded but well-formed authorization came back 402 with invalid_exact_evm_insufficient_balance.
The 400/402 descriptions quoted an error string from inside the x402 library. It was accurate, but it is an upstream implementation detail that a dependency bump could change and that a reviewer cannot check from this repository. Both are now described by observable behaviour and each cites the artifact here that shows it: the unpaid action in go-tunnel-e2e-evidence.json is 402, the forged one is 400. Neither claim now depends on a string this repo does not own. A sweep for a 409 without a named path found one the earlier passes had missed. Section 6's table is the in-process relay's and its 409 is right there, but the row did not say so — and by section 9 a reader has three different replay answers in mind. The row is now followed by all three: 400 on the transport demo, 409 in-process, and 202 then DUPLICATE_ACTION on the hosted path, with none of them actuating the robot twice. The same sweep caught the validation report still saying the outbound client "depends on aip-go-sdk and dials the relay over WSS" — the imprecision already corrected in the maintainer comment and the PR body, left standing in the third place it appears. The report now separates gorilla/websocket for the transport from aip-go-sdk for the optional AIP path, so all three agree.
…ded on CI went red on a commit whose only changes were to two documentation files. It had installed MuJoCo 3.12, released since the previous run, and 3.12 settles the resting arm 2.3 cm higher than 3.11 — the hand rests at 0.9816 m where the recorded geometry says 0.9589 m — so test_home_pose_matches_the_recorded_geometry failed. The episode barely noticed: mean position error moved by 0.00001 m, the pelvis floor not at all, and the task still reported 3/3 on both engines. But the shelf coordinates in task.py were chosen from a reach envelope measured at that resting pose, so the committed geometry and the engine that produced it have to travel together. requirements.txt floored mujoco, numpy and pybullet without bounding them, which meant a reviewer cloning the repository would get today's releases rather than the ones docs/evidence was recorded on. They are bounded now. The tempting fix was to widen the test's tolerance. That would have turned a working reproducibility check into a decoration — and the check is what found this, on a documentation-only commit, which is exactly when nobody would have been looking.
Two corrections to the previous commit, both about claiming more than was seen. It bounded numpy and pybullet alongside mujoco. Only mujoco's drift was ever observed; the other two were bounded on suspicion, which costs anyone building this and buys nothing demonstrable. They are floored again. The integrity tests are the net that caught mujoco on a documentation-only commit, and they would catch these the same way. And it said MuJoCo 3.12 settles the arm higher than 3.11, which is a claim about what changed inside the engine and was not established. What was established is narrower and enough: a clean CI environment resolved 3.12.0 and measured the resting hand at 0.9816 m against the 0.9589 m recorded with the evidence build. The report now says that and states explicitly that no cause is being claimed.
The recorder drove its own simulation loop for the frames and then called run_episode again, on a fresh environment, for the metrics and the result that went back to the bridge. So the GIF showed one episode and the numbers beside it came from another, while the PR claimed the two describe the same paid action. Determinism made them agree; agreeing is not being the same run, and the claim was for the second thing. run_episode's on_step now carries the controller's plan, which is what the recorder needed the second loop for, and the recorder renders from inside the shared scored loop. One environment, one controller, one episode: 2303 control steps produce 57 frames at a stride of 40, and the metrics are the committed ones. stop_requested reaches that episode too, so a safe stop now halts the run being recorded rather than a different one. The PR body said POST /action is unchanged and that the tunnel is used unmodified apart from three read-only endpoints. Neither is true: the route refuses an incomplete identity before publishing, answers 202 with an action_id and a status URL where it used to answer 200, and defers settlement to a watcher, with the stock middleware replaced by a gate to allow it. That is a contract change and the diff shows it, so the description says it. Also: mujoco is pinned to 3.11.0 exactly rather than <3.12, because a 3.11.x patch is a different build from the one the numbers were recorded on. A failed result subscription no longer burns a sync.Once and leaves the status endpoint answering pending for the life of the process. And a waiter registered before a publish that then fails is released instead of waiting for an answer that is never coming — with a test that fails if the release is removed.
The committed recording was produced by the version that rendered one episode
and scored another. The code was fixed in the previous commit; this replaces the
artifact it made, so the file and the claim about it now agree.
action atlas-inspect-1787242629
settlement 0x7fc1c1ff21e535376bf22b3409b751d769dadfdcb2491d3803c5712905b5b197
block 45737187, 0.001 USDC
nonce 0x97b7245b38f9b8d38bd14485f3a912f57621039933d21480dae6fb7a44aa0c40
Located on chain by that nonce rather than by the hash: keccak256 of the action
id is enough to find the AuthorizationUsed event, which is the binding used in
the direction a reviewer would use it — start from the action, arrive at the
payment.
The frames now come from inside the scored run, so the numbers beside the robot
are that robot's, not a second episode's that happened to agree.
|
@Junzhe — this Atlas Tier 1 submission is out of draft and ready for review at What passes at that exact HEAD. Five jobs, green on the author fork — run 32391512680: unit and contract tests; the inspection episode scored on MuJoCo, PyBullet and Webots R2025a; the Go tunnel built from a clean checkout with its payment-contract tests; the x402 payment gate; and a secret scan over the diff. Two of those steps are live rather than replayed — the x402 facilitator is called and must refuse a forged authorization, and the settlement is re-read from a public Base Sepolia RPC and checked against what this profile declares. What CI does not do, stated plainly. The paid request itself is not executed inside a workflow run. It was run against the hosted Fabric relay from an operator machine, since this fork holds no payment secrets. That settlement is verifiable without trusting the artifact: the EIP-3009 authorization nonce is If an exact-HEAD run in a trusted context that performs the paid request itself would help your verification, I can add a push-gated job for it. This PR's own checks tab is empty because workflows on fork pull requests wait for maintainer approval. The boundaries that are not demonstrated — the |
The settlement this profile shows was real, verified on chain, and produced on an operator machine. That last part is the weakness: a reviewer had to trust an artifact rather than watch the payment happen at the commit being reviewed. This adds a job that performs it in the run itself — the tunnel built from this checkout dials the hosted relay, the robot is discovered, the price is read from the catalogue, the relay answers 402, an EIP-3009 authorization is signed with keccak256(action_id) as its nonce, Atlas executes, and settlement follows the correlated result. The refused action runs in the same job and settles nothing. It is gated on push and workflow_dispatch because secrets do not exist in a workflow triggered from a fork's pull request, and it opts out of cancel-in-progress so a settlement in flight is never cut in half. Without the credential it still rehearses the entire path with --dry-run, signing nothing, so a broken harness surfaces before money does rather than after. The key is read from the environment by code that already refuses to print, log, or write it. Nothing in this commit contains one.
The signer reads a private key or a mnemonic and refuses to print either. The workflow wired only the first, so a repository holding the second would have skipped the paid steps while still reporting green — the worst kind of pass. Both are wired, and the gate treats either as sufficient. The three commands are also written out again: a heredoc had collapsed their continuations into one long line. Same command, legible to whoever reads the workflow to check what the paid step actually runs.
Two defects in the job added two commits ago, both found by reading the evidence
it produced rather than its colour.
`python … | tee file` exits with tee's status, which is always zero. The refused
run recorded two broken invariants — "the status endpoint reported the action
failed" was false, because the action had succeeded — and the step reported
success anyway. A job that cannot go red is not a check. All three steps now set
pipefail.
The refused step also never asked for a refusable action. --expect-failure
asserts that the episode failed; it does not cause it. With the default 30s
budget the inspection completed normally, so the assertion was bound to break.
It now sends 1s, outside the 5..60 the catalogue declares, which the bridge
refuses before the robot moves.
Nothing settled for that action either way: authorizationState for
keccak256("atlas-inspect-1787248056") is still zero on chain.
The previous commit message said all three steps set pipefail. Two did. The rehearsal was left as it was because the edit that was supposed to change it silently matched nothing, and the claim went in unchecked — the same shape of mistake the commit was fixing. It is set now, and the command is written across lines again rather than collapsed into one.
|
Following up on my previous comment, which said the paid request is not executed inside a workflow run. That is no longer true, and the correction is the point of this note. The In run 32399852659 at
Neither needs the artifact to be believed: Two things worth stating rather than leaving to be noticed. The job is gated on Nothing else changed: the recording, manifest, checksums and settlement evidence are as they were. |
|
@shicaih @fabricfoundationdev Add robot profile: boston-dynamics/atlas/boston-dynamics.atlas.mujoco-pybullet-webots-shelf-inspection.v1 — Boston Dynamics Atlas v4, simulator-only, MuJoCo + PyBullet + Webots R2025a from one pinned URDF. All six checks pass on the exact HEAD in run 32399852659, and the paid request is performed inside that run rather than alongside it. Every hash in that run's evidence is one of three kinds, and each recomputes:
The refused action asks for a 1-second budget against the 5..60 the catalogue declares, so the bridge answers The boundaries this does not demonstrate are written out at the end of the description rather than left implicit: the Happy to add anything further that would help you verify it. |
Submission-window clarificationHi maintainers — Atlas Tier 1 is ready for review and has been publicly submitted before the Aug 21 date referenced for the bounty. Could you please confirm whether the bounty window requires the PR to be opened/submitted before the deadline, or whether it must be merged before then? PR #120 currently has the complete implementation, committed evidence, passing CI, simulator validation, and verified Base Sepolia settlement evidence. If there is any remaining blocker for Atlas Tier 1, please point it out and I'll address it immediately. |
Boston Dynamics Atlas — Tier 1: Paid Multi-Step Shelf Inspection
Payment-gated, policy-driven shelf inspection on a free-standing Boston Dynamics
Atlas v4, executed through the RoboPay → Zenoh → simulator path and validated
across MuJoCo, PyBullet and Webots R2025a from one pinned robot description.
One pass over one action, nothing edited together. The left pane is the real
exchange as it happens — skill and price discovered, the unpaid 402, the
EIP-3009 authorization signed with
keccak256(action_id)as its nonce, the202, the action on Zenoh, the correlated result, and 0.001 USDC settled
with its BaseScan link and the token's own confirmation that the authorization
was spent. The right pane is the episode that payment bought, rendered from
inside it: the frames are drawn from a callback on the scored run's own control
loop, so the two panes cannot be two different runs that happened to agree. The
settlement is
0x7fc1c1ff…b5b197,block 45737187, and you can reach it without taking my word for the hash —
keccak256("atlas-inspect-1787242629")is the authorization nonce, which isenough to find the
AuthorizationUsedevent on USDC and read the transfer offit.
The task close-up, in MuJoCo. Green spheres are the three targets at their
tolerance radius; the panel is live telemetry. The renderer refuses to write the
GIF unless the annotated episode reproduces the plain one exactly, so the picture
and the numbers cannot disagree.
TL;DR
action_ida failed execution is accepted asynchronously and never settles
→ live facilitator → Zenoh → Atlas 3/3 → relay status → settlement
price the relay quotes, not from a constant
action_idbykeccak256(action_id), checkable on chainTier 1 success criteria
validation-report.mdsim2sim-validation.jsoncontrol_core.pytest_controller_is_closed_loop_not_a_replayed_trajectorytask.pymujoco-inspection-episode.jsonsim2sim-validation.jsonrobot/tunnel/action, executed, answered onrobot/tunnel/resulttunnel-e2e-evidence.jsongo-tunnel-e2e-evidence.jsoninvalid_exact_evm_signature; nothing executestests/test_facilitator.pytests/test_idempotency.pyEnd-to-end flow
demo_tunnel.pyruns exactly that over the real transport (Zenoh peer mode, norouter required). Every request in the walkthrough:
txHash† Replay answers differently on each path, because each one answers at a
different moment:
demo_tunnel.py, Python client over Zenoh)400demo_e2e.py, answers synchronously)409demo_fabric_e2e.py)202, thenDUPLICATE_ACTIONon the status endpointThese are three distinct transport and execution paths. All three reject a
replayed execution, and none actuates the robot a second time. The skill
contract names each case separately rather than leaving the difference to be
inferred from the code.
† What the accepting row is, and where the real one lives. That receipt is
a synthetic hash, so the row shows the bridge's protocol checks passing —
amount, format, replay, declared parameters — and the action being executed and
answered. This walkthrough holds no wallet: it verifies no signature with
the facilitator and moves no value, and its artifact says so
(
payment_verification: protocol_checks_only,settlement: eligible_not_on_chain). Its job is the transport and the refusal paths; theforged authorization here is refused by the live x402 facilitator.
Accepted live payments are proven separately, twice, and both settle real USDC:
real-paid-run.json(facilitatorisValid: true→ 3/3 → 0.001 USDC) andfabric-relay-e2e.json(the same, through the hosted relay). Nothing in thissection should be read as the profile's evidence for a real payment.
Two properties the demo asserts on itself: an unverified payment never reaches
Zenoh, and every executed action is answered carrying the originating
action_id,robot_id,skill_id,params_hashandidempotency_key.Through the hosted Fabric relay, with nothing stood in for
fabric-relay-e2e.jsonis the run where every component is the real one — thehosted relay at
api.fabric.foundation, this repository's Go tunnel dialled outto it over WSS, the live facilitator, Zenoh, and MuJoCo:
GET /robots/{id}/skills→ 200, robot connectedinspect_shelf,stop1000raw, matching the discovered priceaction_idand a status URLsucceeded, correlated byaction_id, read back from the relay0xfd9eda75…1e6940, block 45714728authorizationState(...) = true— the authorization was spentkeccak256("atlas-inspect-1787197727")The price is discovered, not assumed. The payment is built from the amount
the relay quotes in its 402, and the run asserts that amount equals the price the
catalogue publishes — a profile whose advertised price had drifted from what its
tunnel charges would fail that check rather than pass quietly.
What this changes in the tunnel, stated in full. Three read-only endpoints
are added —
GET /robot,GET /skills,GET /action/:action_id/status— andPOST /actionis extended, not left alone: it keeps the existing x402verification boundary, but now refuses an incomplete identity before publishing,
answers
202 Acceptedwith anaction_idand a status URL instead of200, anddefers settlement until the correlated result reports success. The stock x402
middleware is replaced by a gate that performs the same
402/verify half andhands the settlement to that watcher. Calling this read-only would be
understating it, and the diff would say so.
The status endpoint is not synthesised: the tunnel subscribes to the same
robot/tunnel/resulttopic the simulator publishes on and stores what arrives,keyed by
action_id; an unanswered action readspending, a failed onefailed.Merged robot-profile branches extend the tunnel the same way.
A failed action is not paid for, and the token says so. The same relay,
the same wallet, an action refused by the catalogue's declared bounds
(
atlas-inspect-1787197752): the tunnel answers 202, nothing settles, andauthorizationState(payer, keccak256(action_id))on the USDC contract isfalse. "We recorded no transaction hash" is an absence of evidence; the
token's own map of spent authorization nonces is not, and because the nonce is
derived from the action id anyone can recompute it and ask USDC directly.
That on-chain artefact is one action. The invariant behind it — an unsuccessful
execution never settles — is held at the point where the decision is made, and
each case has a test that fails if the guarantee is removed:
TestAFailedEpisodeIsNeverSettled·test_valid_payment_failure_no_settlementTestASilentRobotIsNeverSettledtest_execution_exception_no_settlementtest_replay_rejected·test_no_double_settleTestSettlementFollowsSuccess·test_settle_on_success_onlyTestAFailedPublishLeavesNoWaiterBehind— 502, no waiter, no settlementThe Go cases run against the settlement gate itself, which is the component that
decides; the Python cases run against the bridge that produces the result it
decides on. Both suites run in CI on every commit.
This is enforced rather than hoped for, and without giving up the immediate
202the tunnel contract promises. The stock x402 middleware settles as soon asa protected route answers anything under 400, so a
202would charge the payerbefore the robot had run. The tunnel replaces it with a gate that keeps the
402/verify half unchanged — an unpaid request still gets402with theadvertised requirements, a payment the live facilitator rejects still never
reaches the robot — and hands a settlement callback to the handler instead of
settling.
POST /actionpublishes, answers202, and a background watcherinvokes that callback only when the correlated result reports success. A
failure or a silent robot leaves the authorization signed and unspent. Eight
tests in
handlers_test.gohold that with no wallet and no chain, includingthat a request with no
action_idis refused before anything reaches Zenoh.Through this repository's own Go tunnel, in isolation
demo_go_tunnel.pyexercises the refusal paths against the tunnel alone, behinda minimal stand-in for the relay:
The payment decision there is the tunnel's, not this profile's. Build steps are
in
TUNNEL_BUILD.md.Two payment gates, and which one proved what
shape, replay. Cheap, and they reject the obvious cases first.
authorization from a well-formed forgery, because only the facilitator
recovers the signer.
The forgery case is proven against the live facilitator: a payload with the
right amount, asset, network and a perfectly shaped signature is refused with
invalid_exact_evm_signature. A companion test asserts the uncomfortable half —the protocol checks alone do accept that same payload, which is precisely why
the facilitator layer exists. Verification fails closed: an unreachable
facilitator is a rejection, never an approval.
The accepting side, proven.
real-paid-run.jsoncarries one action thewhole way: an EIP-3009 authorization signed by a funded wallet, accepted by the
live facilitator (
isValid: true), executed on the robot (3/3 targets,correlated by
action_id), and settled for 0.001 USDC — the price theprofile declares — in
0x2b3b71d0…c0f39, block 45706216.The settlement is tied to the action rather than merely adjacent to it. EIP-3009
lets the signer choose the authorization nonce, so this run sets it to
keccak256(action_id); the token emits that nonce inAuthorizationUsed, soanyone can recompute it from the action id and confirm this transfer paid for
this action:
The transaction was submitted by the facilitator's own address — the one its
/supportedendpoint advertises — which is independent evidence it went throughthe live facilitator rather than being self-submitted, and why the payer holds
no ETH: under EIP-3009 the payer signs and the facilitator pays the gas.
Order matters and is enforced:
/verifyfirst, robot second,/settleonlybecause the episode reached every target. A failed episode leaves the
authorization signed and unspent.
The in-process and Zenoh demos remain protocol-level by design — they hold no
wallet — and their artifacts say so:
settlement: eligible_not_on_chain,settlement_tx_hash: null. That distinction is enforced by the ledger, not bywording:
SETTLEDrequires a transaction hash and its block, andtest_settled_requires_a_real_transactionfails if a receipt alone can earn it.One actuation per payment-validated action
The idempotency store is keyed on
robot_id + skill_id + idempotency_key,records the parameters and a payment fingerprint, and persists to disk:
duplicate— recorded outcome replayed, robot does not moveIDEMPOTENCY_PARAMS_CONFLICTIDEMPOTENCY_PAYMENT_CONFLICTController
Each control tick re-reads the measured end-effector pose and the measured joint
configuration, then solves one damped-least-squares resolved-rate step on the
right-arm chain. A target only counts after the hand holds it inside tolerance
for 250 consecutive steps. No recorded trajectory is used anywhere.
Atlas is never welded, clamped or externally supported. The fall check uses real
standing height — 0.70 m against a 0.911 m stance — not floor contact.
Results
Cross-engine agreement: mean-error spread 3.20 mm (limit 50 mm), duration
spread 3.10 s (limit 5.0 s) → sim-to-sim verdict PASS.
Repeated MuJoCo runs hash identically — the test fingerprints the whole result,
not a few metrics.
The robot
Atlas v4 is fetched, never vendored:
openai/roboschoold32bcb2b35b94168b5ce27233ca62f3c8678886fatlas_description/urdf/atlas_v4_with_multisense.urdfNOTICE.mdThe Jacobian and the gravity feedforward come from that URDF via
kinematics.pyrather thanfrom each engine, so the controller is identical everywhere. Tests pin both
against MuJoCo's independently computed values (Jacobian within
5e-4, measuredworst case
2.4e-4; gravity model within1e-3N·m).Actuator addressing is read out of the compiled model and validated against the
URDF's own effort limits; any drift raises immediately.
Payment
One price and one asset address, declared once and asserted against every
consumer:
1000raw units0x036CbD53…3dCF7eeip155:84532A settlement reference that is not a 32-byte EVM transaction hash is refused with
MALFORMED_TX_HASHbefore anything executes.On-chain settlement
act-paid-de66513f791b0x2b3b71d0…c0f390x1), block 45706216Transfer(address,address,uint256)on the USDC contract1000raw) — the price the catalogue publishes0xa0597a74…Fc2Dc0x7b916325…C3e8keccak256(action_id)settlement_evidence.pyreads this transaction out ofreal-paid-run.jsonrather than from a pinned hash, re-reads it from a public RPC, decodes the
Transferlog itself, and exits non-zero if it is missing, reverted, carries no USDCtransfer. The workflow runs it on every push.
Evidence
atlas-paid-action.gifatlas-shelf-inspection.gifmujoco-inspection-episode.jsonpybullet-inspection-episode.jsonwebots-inspection-episode.jsonsim2sim-validation.jsonreach-envelope.jsongo-tunnel-e2e-evidence.jsongo-tunnel-e2e-terminal.txtfabric-relay-e2e.jsonfabric-relay-failure.json202, reportedfailedwith its real reason — and never charged, confirmed by the token contractreal-paid-run.jsonaction_idtunnel-e2e-evidence.jsontunnel-e2e-terminal.txtdemo-e2e-evidence.jsononchain-settlement.jsonSkill discovery and pricing are published in
skill-catalog.json,generated from
skills.yamlso the two cannot disagree.Every committed evidence artifact is checksummed in
evidence-manifest.yaml,and stored byte-exact so the recorded hashes still match after a clone.
Why the shelf is where it is
reach_envelope.pydrives the arm to a 6 × 6 grid of offsets and records, perprobe, whether the arm converged and whether Atlas was still standing:
The reported envelope is the largest block where every probe succeeded
(forward 0.06–0.18 m, vertical −0.12…+0.20 m, 15/15) — not a bounding box around
scattered successes. All three targets sit inside it, and a test fails if one is
ever moved out.
Reproduce
Configuration, expected success and failure payloads, safe-stop and
troubleshooting are documented in the
bridge README.
Tasks
action_idand the tunnel's identity fieldsparams_hashin the publishedsha256:<hex>form for every actionaction_idfailedthrough the relay, with its real reasonfunctions.yamlcontract match every route's real responsesaction_idthe speed reached while inspecting separately from the episode peak
Reviewer notes
Webots was validated locally, headless. It is not available on GitHub's
runners, so CI verifies that the PROTO and the world still generate from the
pinned URDF, and
sim2simreports the engine asunavailable_no_webots_installationthere, scoring only the engines that ran.A missing engine can never turn a failing comparison into a passing one.
CI status. Commit
2b09fc2, the exact HEAD of this PR, passed every job onthe author fork —
run 32399852659,
which includes the Zenoh tunnel walkthrough, the on-chain settlement
re-verification, and the paid action itself on a clean Ubuntu runner. The
upstream PR workflow shows
action_requiredand awaits maintainer approval,which is the normal path for a fork; that is why this PR's own checks tab is
empty.
The payment happens in the run, not beside it. The
livejob builds thetunnel from the checkout, dials the hosted Fabric relay, discovers the robot
and the price, takes the
402, signs an EIP-3009 authorization whose nonce iskeccak256(action_id), runs Atlas, and settles only after the correlatedresult reports success — all inside the run being reviewed. In
32399852659 that
was
atlas-inspect-1787248406, settled in0x30160596…c5a32at block 45740066. The same job then sends an action the catalogue refuses — a
1-second budget against a declared 5..60 — which comes back
failedwithINVALID_DURATIONand settles nothing:authorizationStateforkeccak256("atlas-inspect-1787248427")is still zero. Both are reachable fromthe action id alone, without trusting the artifact. The job runs on
pushandworkflow_dispatchonly, because secrets do not exist in a workflow triggeredfrom a fork's pull request; without a credential it still rehearses the whole
path with
--dry-run, signing nothing.Where the three engines genuinely differ. Only the joint servo: MuJoCo
integrates an explicit PD law, while PyBullet and Webots use their own implicit
servos, all saturated at the same URDF effort limits. Webots' servo gain had to
be raised from its default — measured, not guessed. Everything above the servo
is shared code.
Every numeric claim above is generated from a committed evidence artefact.
Four demos, and exactly what each one runs. Only the first reaches the
hosted relay; the others isolate a layer, and none of them stands in for a
component the first one exercises.
demo_fabric_e2e.pydemo_go_tunnel.pydemo_tunnel.pyreal_paid_run.pyKnown boundaries
description this PR pins —
atlas_v4_with_multisense.urdffromopenai/roboschool@d32bcb2, fetched and SHA-256 verified at build time — andno claim is made that it is equivalent to, or interchangeable with, any other
Atlas hardware generation.
this repository. Both payer wallets are disposable test wallets, treated as
compromised, holding nothing of value.
demo_go_tunnel.pystill stands in for it when exercising the tunnel'srefusal paths in isolation. The positive path does not stand in for anything.
are disposable test wallets, treated as compromised, holding nothing.
conflated, so, from the code: there is no package called
robotsdkin thisrepository. The robot-side outbound client it ships is
tunnel/, and therelay connection itself is
tunnel/internal/client.go, which dials the WSSendpoint with
gorilla/websocket. The same module also carriesgithub.com/unibaseio/aip-go-sdk, used bycmd/main.goandinternal/aipagentfor the optional authenticated AIP path — not for that transport.
tunnel/isthe component the merged Tier-1 profiles use. This profile extends its action
contract —
202/status semantics and deferred settlement — and adds threeread-only discovery and status endpoints; it changes no transport behaviour. The authenticating handshake lives in it: with
AIP_ENABLED=truethe tunnel runs
aipauth.EnsureAuth, receives a bearer token and a walletaddress, and registers the agent with
Handle= robot id andUserID= thatwallet. This profile does not bypass it, but the recordings here run with it
disabled, because
EnsureAuthdrives an interactive browser authorization flowthat an unattended, reproducible demo cannot perform. So: implemented in the
component this profile uses, not exercised in these recordings, and not
reimplemented here. The payee half is held and tested —
GET /robot, the402and the settlement all carry the one configured address, and the x402matcher refuses a payment whose payee differs. And the binding that is
cryptographic is the settlement to the action:
keccak256(action_id)as theauthorization nonce, recorded on chain.
main.#59 moved the tunnel's
x402 wiring onto
ginmw.SchemeConfigwith theevm/exact/serverscheme andadded
registerTokenAssetfor non-default payment tokens;#126 added MPP
alongside x402 on the same
POST /action. This branch keeps the tunnel APIthat the recorded evidence and CI were produced against, so merging
maininto it conflicts in
tunnel/cmd/main.go,tunnel/internal/handlers/handlers.goandtunnel/internal/handlers/handlers_test.go— the deferred settlement gatesits exactly where those PRs rewrote the middleware call and the handler.
The Zenoh envelope is not a problem: Add MPP (Machine Payments Protocol) support alongside x402 #126 keeps
payment_payloadandpayment_requirementswhere they were and only addsprotocol, and thisprofile's bridge reads those two keys and ignores what it does not know, so
an envelope produced by the newer tunnel parses here unchanged. That is an architectural
reconciliation, not a formatting one, and it is deliberately not attempted
here: doing it would rebuild the component every artifact in this submission
was recorded against. No claim is made that the two tunnel implementations
are drop-in compatible. The Tier branch this PR targets sits at the commit
the profile was built on, which is why it merges cleanly there. What has been
checked is the conflict itself; the work to rebuild the gate on the newer
scheme registration has not been scoped, and can follow on request.
as
name/priceUSDC/paramsSchema, and a failure envelope asstatus: "error"carrying a nestederror.code. This profile uses therepository's existing snake_case forms instead:
skill_id,price_usdcandparamsin the catalogue,robot_idin the/skillsenvelope, andstatuswith
result.error_codein the correlated result. Every concept the criterianame is present and carried by the recorded artifacts and the contract tests;
these are naming choices, not missing data. They are left alone because the
evidence was recorded against them, and renaming the wire format afterwards
would put the artifacts and the code out of step.
params_hashis derived, not carried. The Zenoh envelope preservesaction_id,robot_id,skill_id,idempotency_keyand the payment. Thebridge computes
params_hashitself, canonically, from the parameters thatarrived, and emits it in the
sha256:<hex>formexecution-mapping.yamldeclares — including for an empty parameter set. A hash supplied on the wire
is not consulted. So the hash in every result describes what actually
executed, and a caller cannot claim one parameter set while sending another;
but it is a derivation rather than a comparison against a payer-supplied
value.