Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
782b2c4
feat: add door-arm-001 skill (open_door) with MuJoCo + PyBullet backends
Aug 10, 2026
ab1a722
feat(unitree-g1): Tier 1 humanoid walker with x402 payment
Aug 13, 2026
ac979ab
fix(unitree-g1): replace synthetic x402 evidence with REAL Base Sepol…
Aug 13, 2026
4367647
cleanup: drop fabric/door/go2/tron1 cross-robot strays; fix G1 docstr…
Aug 13, 2026
a3693ad
rebuild planar biped: g1_spec.py + MuJoCo simulator (kinematic feet, …
Aug 13, 2026
bf17802
feat(unitree-g1): planar biped PyBullet backend + executor/demo/profi…
Aug 13, 2026
310790e
fix(unitree-g1): align planar-biped contract and USDC address
Aug 13, 2026
c0a4473
fix(bridge/tests): exclude docs/ from private-key scan (real tx hash …
Aug 13, 2026
e5950d4
cleanup(unitree-g1): drop door-arm strays from PR, restore upstream g…
Aug 13, 2026
42b912b
feat(unitree-g1): real Go Tunnel payment gate + registry catalog (Tie…
Aug 13, 2026
cc20237
ci(unitree-g1): real Go Tunnel tunnel-integration job + Python 3.11
daluoboda Aug 13, 2026
42c6173
feat(unitree-g1): complete registry profile + traceability + runbook …
daluoboda Aug 14, 2026
0e1d92f
fix(ci): python 3.11 + evidence job path + Pillow dep + README profil…
daluoboda Aug 14, 2026
ab91b9a
feat(unitree-g1): shared bridge — loco + pick-and-carry, 4 skills, on…
daluoboda Aug 14, 2026
4cc0649
revert(unitree-g1): restore loco-only bridge; split pick-and-carry in…
daluoboda Aug 14, 2026
f3c0baf
feat(unitree-g1): add pick-and-carry Tier-1 skill stacked on #90
daluoboda Aug 14, 2026
d624284
docs(evidence): continuous visual-evidence clip + captured manifest (…
daluoboda Aug 17, 2026
1f6f67a
docs(receipt): normalize x402-evidence.json tx hashes to 0x-prefixed …
daluoboda Aug 17, 2026
d135b86
chore(#96): bind independent real Base Sepolia tx for pick_and_carry
daluoboda Aug 17, 2026
351ebcb
chore(#96): bind independent real Base Sepolia tx for pick_and_carry
daluoboda Aug 17, 2026
05a8d59
chore(#96): bind independent real Base Sepolia tx for pick_and_carry
daluoboda Aug 17, 2026
a60976a
chore(#96): bind independent real Base Sepolia tx for pick_and_carry
daluoboda Aug 17, 2026
f5a6a81
chore(#96): bind independent real Base Sepolia tx for pick_and_carry
daluoboda Aug 17, 2026
d7471f8
chore(#96): bind independent real Base Sepolia tx for pick_and_carry
daluoboda Aug 17, 2026
6455cac
chore(#96): bind independent real Base Sepolia tx for pick_and_carry
daluoboda Aug 17, 2026
1a86a16
chore(#96): bind independent real Base Sepolia tx for pick_and_carry
daluoboda Aug 17, 2026
6658fbe
chore(#96): bind independent real Base Sepolia tx for pick_and_carry
daluoboda Aug 17, 2026
a52d986
chore(feat/unitree-g1-pick-and-carry-tier1): add actionId top-level f…
daluoboda Aug 17, 2026
f9ac4c6
docs(96): add R7 controller-narrative VALIDATION.md (state-machine / …
daluoboda Aug 17, 2026
92b17ba
fix(manifest): add claimBoundary + quote notes
daluoboda Aug 17, 2026
f020c66
evidence(g1-pc): add 2D physics recording + sync manifest (robopay_ev…
daluoboda Aug 17, 2026
9cc036a
docs(g1-pc): record commit_sha f020c66f73 in evidence manifest
daluoboda Aug 17, 2026
262e919
evidence(robopay_evidence.gif): add R11 overlay panel
daluoboda Aug 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/secret-scan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: secret-scan
on:
push:
branches: [main, audit, feat/*]
pull_request:

permissions:
contents: read

jobs:
gitleaks:
name: Detect hardcoded secrets
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout (full history)
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Run gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Tighter scope: only flag HIGH-confidence hits; tune allowlist
# locally with .gitleaks.toml if a known-safe pattern triggers.
GITLEAKS_ENABLE_SUMMARY: true
67 changes: 67 additions & 0 deletions .github/workflows/unitree-g1-bridge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: unitree-g1 Tier 1 bridge

on:
push:
branches: [main, master]
pull_request:
branches: [main, master]

jobs:
lint:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install -r bridge/unitree-g1/requirements.txt
- run: python -m flake8 bridge/unitree-g1 --max-line-length=100
- run: python -m mypy bridge/unitree-g1 --ignore-missing-imports

test:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install -r bridge/unitree-g1/requirements.txt
- run: pytest -q bridge/unitree-g1/tests/

tunnel-integration:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install -r bridge/unitree-g1/requirements.txt
- run: pytest -q bridge/unitree-g1/tests/test_x402.py

sim2sim:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install -r bridge/unitree-g1/requirements.txt
- run: pytest -q bridge/unitree-g1/tests/test_sim2sim.py

evidence:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install -r bridge/unitree-g1/requirements.txt
- run: pip install pillow # render_evidence.py needs PIL
- run: python -m flow.demo --all
working-directory: bridge/unitree-g1
- run: python docs/evidence/render_evidence.py
working-directory: bridge/unitree-g1
- name: independently verify the Base Sepolia settlement receipt
run: python verify_settlement.py
env:
X402_EVIDENCE: bridge/unitree-g1/docs/evidence/x402-evidence.json
262 changes: 262 additions & 0 deletions bridge/unitree-g1/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
# unitree-g1 — RoboPay Tier 1 bridge (Simulator Skill Execution)

A paid `pick_and_carry` / `stop` skill executed by **real physics**, driven over
**Zenoh**, paid with **x402**, and settled **only when the robot actually
succeeded**. This is a **humanoid pick-and-carry** task (Tier 1, B1), built on
the same G1 bridge that previously carried a plain-walk skill — deliberately
kept on a distinct `pick-and-carry.v1` profile so it does **not** collide with
the `#24` obstacle-avoidance track or the old `#90` walk track.

| | |
|---|---|
| robotId | `unitree-g1` |
| profileId | `laok.unitree-g1-arm-001.pick-and-carry.v1` |
| skills | `pick_and_carry`, `stop` |
| engines | MuJoCo (primary) + PyBullet (sim-to-sim) |
| transport | Zenoh — `robot/tunnel/action` / `robot/tunnel/result` |
| scope | **simulation only** — CPU, headless, no GPU, no ROS, no hardware |

> **Scope statement (criterion #6).** This bridge never drives physical
> hardware. There is no motor driver, no teleop channel and no hardware SDK in
> the dependency list. Every action runs inside a physics engine in-process.

> **Track separation.** `pick_and_carry` is the only locomotion/actuation skill
> here. The `#24` `navigate_obstacle` (curb-crossing) and the `#90` plain
> `move_forward` walk skills live on different profiles; this PR adds a new
> pick-and-carry capability without touching them.

---

## 1. Quick start (< 5 minutes)

```bash
cd bridge/unitree-g1
python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt

pytest -q # full test suite
python -m flow.demo --all # the paid flow, all scenes
```

`requirements.txt` is CPU-only. MuJoCo and PyBullet both ship manylinux wheels,
so there is nothing to compile on `ubuntu-22.04` (the CI reference platform).

> **Windows note.** `zenoh` and `pybullet` publish no Windows wheels. On Windows
> the demo runs over the loopback transport with MuJoCo — same envelopes, same
> topics, same payment path. Use Linux (or the CI workflow) for the real Zenoh
> session and the PyBullet cross-check.

## 2. What the demo prints

```
skill status settled dist(m) steps
------------------------------------------------------------------------------
pick_and_carry completed True 2.0002 957
stop completed True 0.0002 50
pick_and_carry {'dropDistance': 8.0}failed False 2.0884 1000
==============================================================================
PASS: every success settles, the genuine timeout does not.
```

`dist` and `steps` are read out of the physics engine: the robot is a planar
biped whose forward displacement comes from real MuJoCo friction contacts
between the planted foot and the ground, plus a 2-link inverse-kinematics swing
foot. A replayed animation cannot produce that column — the torso position is
taken straight from the solver's body coordinates. The object is modelled as
co-located with the torso (a box the biped carries), so `carried` flips to
`True` once the pickup zone is passed and stays set through the carry.

> The numbers above are the **actual** output of `python -m flow.demo --all`
> on this repository (MuJoCo 3.11, single thread). They are deterministic: the
> same machine produces the same rows every run.

## 3. Flow

```
flow/demo.py CLI client (no LLM, no agent)
│ 1. list_skills free, from profiles/skills.yaml
│ 2. request_action ── 402 ──▶ x402 accepts block, robot untouched
│ 3. pay ── X-PAYMENT receipt ──▶
flow/relay.py verify → validate params → dispatch → settle/skip
│ six-field envelope (flow/envelope.py)
flow/zenoh_transport.py publish robot/tunnel/action
flow/node.py unitree-g1 robot node
flow/executor.py skillId → backend
simulator.py (MuJoCo) | simulator_pybullet.py (PyBullet)
│ both read g1_spec.py — one robot definition
result + metrics publish robot/tunnel/result (correlated by actionId)
flow/payment.py SUCCESS → settle FAILED → no settlement
```

## 4. Zenoh topics

| topic | direction | payload |
|---|---|---|
| `robot/tunnel/action` | tunnel → robot | `actionId, robotId, skillId, idempotencyKey, paramsHash, payment, params` |
| `robot/tunnel/result` | robot → tunnel | `actionId, robotId, skillId, paramsHash, status, message, metrics` |

Results are correlated to requests by `actionId`. Default endpoint
`tcp/127.0.0.1:17447`, mode `peer` — no external router required.

The Go tunnel that fronts this bridge lives in [`tunnel/`](../../tunnel) at the
repository root. It holds the outbound WebSocket to the Fabric proxy, runs the
x402 middleware, and only publishes an accepted action to `robot/tunnel/action`
after the payment verifies — the same topic the bridge subscribes to. Actions
received over that tunnel share the exact envelope and safety path as the demo.

Run the robot node separately:

```bash
python -m flow.node # subscribes to robot/tunnel/action
python -m flow.demo --transport zenoh # in another shell
```

## 5. The robot

`unitree-g1` is modelled as a **planar biped** (sagittal X-Z plane, Z up), defined
once in [`g1_spec.py`](g1_spec.py) and consumed by **both** engines. It carries
**4 actuated joints** — `left_hip`, `left_knee`, `right_hip`, `right_knee` — all
hinge joints in the sagittal plane. The torso is posture-locked: it has only X
(forward) and Z (vertical) translation DOF, never a rotation, so the robot is
deterministically upright.

Skills:
- `pick_and_carry`: walk forward to a **pickup zone** (`pickupDistance`, default
1.0 m), acquire the carried object (modelled as co-located with the torso on
this planar biped), then **carry** it to a **drop zone** (`dropDistance`,
default 2.0 m). Success when the torso reaches the drop zone within the step
budget after passing the pickup zone.
- `stop`: bring the biped to rest and hold both feet planted — the safe-stop
primitive.

Locomotion is produced the only honest way: two 2-link legs step in a fixed,
deterministic gait, the planted foot anchors to the ground through real MuJoCo
friction contacts, and the torso is carried forward by the leg geometry. There is
**no learned policy and no potential field** — `g1_spec.py` is the entire
controller, and it is pure 2-link inverse kinematics plus a step-synced velocity
drive. Nothing about the trajectory is scripted: the forward displacement and the
carry state are read straight out of the physics engine's solved body positions.

### Failure modes (criterion #5)

| scene | outcome | why it fails | settled |
|---|---|---|---|
| `pick_and_carry` | **success** | walked to pickup zone, acquired object, reached drop zone (2.0002 m) | ✅ |
| `stop` | **success** | halted within the budget | ✅ |
| `pick_and_carry {'dropDistance': 8.0}` | `timeout` | a drop distance of 8.0 m is valid per schema (`maximum: 8.0`) but larger than any gait budget can reach (~2.2 m), so the real physics runs the full step budget and exhausts it | ❌ |

The `timeout` row is **not** a parameter rejection — `dropDistance: 8.0` passes
schema validation; it fails because the simulator genuinely cannot carry that
far within the step budget, which is the behaviour criterion #7 wants to see.

## 6. Payment safety (criterion #7)

* No payment → `402` with the x402 `accepts` block. **The robot is never
contacted** — the demo prints the execution counter to prove it.
* Payment without a well-formed `txHash` → `402`, still no execution.
* Invalid or unknown parameters → rejected **before** dispatch, no settlement,
and the idempotency key is not consumed.
* Execution failed → `paymentState: FAILED`, `settled: false`. Settlement is
skipped, not reversed: nothing is ever captured up front.
* Replayed `idempotencyKey` → `rejected`, no second execution, no second
settlement.

Proof lives in `tests/test_flow.py`, `tests/test_simulator.py`,
`tests/test_profiles.py`, `tests/test_payment_gate.py`,
`tests/test_x402_no_settlement.py` and `tests/test_sim2sim.py`.

## 7. Profiles — loaded, not decoration

| file | purpose |
|---|---|
| [`profiles/robot.profile.yaml`](profiles/robot.profile.yaml) | identity, scope, kinematics, transport, wallet env binding |
| [`profiles/skills.yaml`](profiles/skills.yaml) | skill definitions, price, params schema |
| [`profiles/functions.yaml`](profiles/functions.yaml) | API functions + rejection rules |
| [`profiles/payment-policy.yaml`](profiles/payment-policy.yaml) | x402 provider, lifecycle, safety switches |
| [`profiles/execution-mapping.yaml`](profiles/execution-mapping.yaml) | topic → handler, skill → actuators |

`flow/profiles.py` reads them at runtime: the price in the 402 challenge and the
parameter validation both come from these files. `tests/test_profiles.py`
compares every number against `g1_spec.py` and the transport module, so a
profile can never drift from the robot it describes.

## 8. Sim-to-Sim

The same skill definition runs on two independent engines:

```bash
pytest tests/test_sim2sim.py -q
```

* **static agreement** — the URDF given to PyBullet and the MJCF given to MuJoCo
are generated from the same `g1_spec.py`; the tests assert identical joint
chains, link offsets and actuator axes.
* **dynamic agreement** — with PyBullet installed, both engines must return the
same verdict, the same failure reason, and an identical metric schema
(`reached`, `pickupReached`, `carried`, `objectX`, `pickupX`).

On Windows those dynamic checks are skipped (no PyBullet wheel) and a contract
stub exercises every PyBullet call path instead. CI on `ubuntu-22.04` runs them
for real.

## 9. Environment

| variable | required | purpose |
|---|---|---|
| `UNITREE_G1_PAYTO_ADDRESS` | onchain mode | address that receives settlement |
| `UNITREE_G1_WALLET_ADDRESS` | onchain mode | robot wallet identity |
| `UNITREE_G1_PRIVATE_KEY` | onchain mode | signing key |
| `X402_FACILITATOR_URL` | onchain mode | x402 facilitator endpoint |

> ⚠️ **Never commit key material.** This repository contains no private keys,
> no mnemonics and no `.env` file. Secrets are read from the environment at
> runtime only, are never logged, and never appear in result metrics — a test
> scans the whole bridge for 64-hex-digit literals and fails the build if one
> shows up.

Default mode is `mock`: verification accepts a receipt carrying a `txHash` and
settlement is recorded in a local ledger, so the demo is reproducible offline.
The success/failure branching, idempotency and no-settle-on-failure rule use the
exact same code path in both modes; `verify_payment` and `SettlementLedger` in
`flow/payment.py` are the only two swap points for live Base Sepolia settlement.

## 10. Layout

```
bridge/unitree-g1/
├── g1_spec.py robot definition shared by both engines
├── simulator.py MuJoCo backend
├── simulator_pybullet.py PyBullet backend (sim-to-sim)
├── flow/
│ ├── demo.py CLI client — the paid flow
│ ├── relay.py 402 / verify / dispatch / settle
│ ├── payment.py payment state machine + settlement ledger
│ ├── envelope.py six-field task envelope
│ ├── executor.py skillId → backend factory
│ ├── zenoh_transport.py Zenoh + loopback, one envelope contract
│ ├── node.py robot node entrypoint
│ └── profiles.py manifest loader (price, schema, policy)
├── profiles/ the five required YAML manifests
├── tests/ test suite
├── docs/ documentation and evidence
└── requirements.txt
```

## 11. Non-goals

No LLM or agent layer, no web dashboard, no ROS2, no GPU, no reinforcement
learning, no multi-robot fleet, no real hardware. The demo client is a plain
CLI on purpose: the thing under review is the paid execution path, not a
product.

---

See [`docs/validation-report.md`](docs/validation-report.md) for the
criterion-by-criterion self-audit.
7 changes: 7 additions & 0 deletions bridge/unitree-g1/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Make the bridge package importable when pytest is launched from anywhere."""
import os
import sys

_ROOT = os.path.dirname(os.path.abspath(__file__))
if _ROOT not in sys.path:
sys.path.insert(0, _ROOT)
Loading