Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Payment-gated access to cluster services via x402 (HTTP 402 micropayments, Traef

**Buy-side flow**: `buy.py probe` sees 402 pricing → `buy.py buy` validates the token contract exists on-chain → pre-signs payment auths (ERC-3009 for USDC, Permit2 for OBOL) into a `PurchaseRequest` CR in the agent namespace → serviceoffer-controller writes buyer config/auth files into `llm` and publishes `paid/<remote-model>` → the in-pod `x402-buyer` sidecar spends one auth per paid request. Agent-managed refill runs through `buy.py process --all`, not the controller.

**buy.py** lives at `${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-inference/scripts/buy.py` inside the agent pod (skill name: `buy-inference`, not `buy`). Commands:
**buy.py** lives at `${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py` inside the agent pod (skill name: `buy-x402`, not `buy`). Commands:
```
probe <endpoint-url> [--model <id>] Probe x402 pricing from a 402 endpoint
buy <name> --endpoint <url> --model <id> Pre-sign ERC-3009 auths + create PurchaseRequest
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ obol sell status <offer-name> -n <namespace>

# 4) Buyer wallet and balances are available.
obol kubectl exec -n hermes-obol-agent deploy/hermes -c hermes -- \
python3 /data/.hermes/obol-skills/buy-inference/scripts/buy.py balance
python3 /data/.hermes/obol-skills/buy-x402/scripts/buy.py balance
```

Run the paid tests only after all four checks pass.
Expand Down
2 changes: 1 addition & 1 deletion flows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ idempotent and exits non-zero on failure.
- `flow-10-anvil-facilitator.sh` — Anvil + Facilitator local test infra (§3). Run BEFORE flow-08.
- `flow-11-dual-stack.sh` — Dual-Stack: Alice sells, Bob discovers via ERC-8004 and buys.
- `flow-12-obol-payment.sh` — OBOL payment asset over the existing USDC commerce baseline.
- `flow-13-dual-stack-obol.sh` — Dual-Stack OBOL: Alice sells, Bob discovers and buys, but the payment asset is a fork-local OBOL ERC20Permit token and the facilitator is a local x402-rs build (not the public Obol facilitator). Use this when you want to validate the OBOL Permit2 path end-to-end without depending on the public Obol facilitator or any USDC contract. Both obol stacks share ONE local Anvil fork of Base Sepolia via `host.k3d.internal:$ANVIL_PORT`. Requires `cast` + `anvil` + `forge` and an `X402_FACILITATOR_BIN` (or `X402_RS_DIR`) pointing at an x402-rs build with `eip2612GasSponsoring`; the script skips with a single PASS if neither is set.
- `flow-13-dual-stack-obol.sh` — Dual-Stack OBOL: Alice sells, Bob discovers and buys, but the payment asset is a fork-local OBOL ERC20Permit token and the facilitator is local (not the public Obol facilitator). Use this when you want to validate the OBOL Permit2 path end-to-end without depending on the public Obol facilitator or any USDC contract. Both obol stacks share ONE local Anvil fork of Base Sepolia via `host.k3d.internal:$ANVIL_PORT`. Requires `cast` + `anvil` + `forge`; the local facilitator runs as `ghcr.io/x402-rs/x402-facilitator:1.4.7`.
- `flow-14-live-obol-base-sepolia.sh` — Live Base Sepolia OBOL dual-stack: Alice registers/sells, Bob discovers/buys, and settlement is verified against the deployed OBOL ERC20Permit token and public Obol facilitator. Requires `REMOTE_SIGNER_PRIVATE_KEY` funded with Base Sepolia ETH and the second deterministic derived Bob key funded with OBOL; `OBOL_TOKEN_BASE_SEPOLIA` defaults to the current live Base Sepolia OBOL token (`0x54AE82bc871a4E3E8E2FE1173Cb864B8563D44D4`) and can be overridden.

`lib.sh` is shared helpers; `release-smoke.sh` is the release gate.
Expand Down
2 changes: 1 addition & 1 deletion flows/flow-01-prerequisites.sh
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ fi
step "remote-signer Helm chart version is published"
rs_version=$(remote_signer_chart_version)
if [ -z "$rs_version" ]; then
fail "Could not parse remoteSignerChartVersion from internal/openclaw/openclaw.go"
fail "Could not parse RemoteSignerChartVersion from internal/agentruntime/charts.go"
elif remote_signer_chart_available "$rs_version"; then
pass "obol/remote-signer $rs_version is available"
else
Expand Down
57 changes: 16 additions & 41 deletions flows/flow-10-anvil-facilitator.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ mkdir -p "$FLOW_STATE_DIR"
ANVIL_LOG="$FLOW_STATE_DIR/anvil.log"
ANVIL_PID_FILE="$FLOW_STATE_DIR/anvil.pid"
FACILITATOR_LOG="$FLOW_STATE_DIR/facilitator.log"
FACILITATOR_PID_FILE="$FLOW_STATE_DIR/facilitator.pid"
FACILITATOR_CONTAINER_FILE="$FLOW_STATE_DIR/facilitator.container"

cluster_facilitator_host() {
if [ -n "${CLUSTER_FACILITATOR_HOST:-}" ]; then
Expand Down Expand Up @@ -131,24 +131,12 @@ if curl -sf http://localhost:4040/supported >/dev/null 2>&1; then
pass "Facilitator already running on port 4040"
FACILITATOR_PORT=4040
else
old_pid=$(cat "$FACILITATOR_PID_FILE" 2>/dev/null || true)
if [ -n "$old_pid" ] && ! kill -0 "$old_pid" 2>/dev/null; then
rm -f "$FACILITATOR_PID_FILE"
fi

# Binary discovery: X402_FACILITATOR_BIN env → ~/Development/R&D/x402-rs
FACILITATOR_BIN="${X402_FACILITATOR_BIN:-}"
if [ -z "$FACILITATOR_BIN" ]; then
X402_RS_DIR="${X402_RS_DIR:-$HOME/Development/R&D/x402-rs}"
for candidate in \
"$X402_RS_DIR/target/release/x402-facilitator" \
"$X402_RS_DIR/target/release/facilitator"; do
[ -f "$candidate" ] && FACILITATOR_BIN="$candidate" && break
done
fi
old_container=$(cat "$FACILITATOR_CONTAINER_FILE" 2>/dev/null || true)
[ -n "$old_container" ] && docker rm -f "$old_container" >/dev/null 2>&1 || true

if [ -z "$FACILITATOR_BIN" ]; then
fail "x402-facilitator binary not found — set X402_FACILITATOR_BIN or build from x402-rs repo"
FACILITATOR_IMAGE=$(x402_facilitator_image || true)
if [ -z "$FACILITATOR_IMAGE" ]; then
fail "x402-facilitator image unavailable: ghcr.io/x402-rs/x402-facilitator:1.4.7"
emit_metrics; exit 0
fi

Expand All @@ -165,37 +153,24 @@ else
"schemes": [{"id": "v1-eip155-exact","chains":"eip155:*"},{"id":"v2-eip155-exact","chains":"eip155:*"}]
}
FEOF
FACILITATOR_PID=$(FACILITATOR_LOG="$FACILITATOR_LOG" FACILITATOR_BIN="$FACILITATOR_BIN" FACILITATOR_CONFIG="$FACILITATOR_CONFIG" python3 - <<'PY'
import os
import subprocess

log_path = os.environ["FACILITATOR_LOG"]
bin_path = os.environ["FACILITATOR_BIN"]
cfg_path = os.environ["FACILITATOR_CONFIG"]

with open(log_path, "ab", buffering=0) as log_file:
proc = subprocess.Popen(
[bin_path, "--config", cfg_path],
stdin=subprocess.DEVNULL,
stdout=log_file,
stderr=subprocess.STDOUT,
start_new_session=True,
close_fds=True,
)
print(proc.pid)
PY
)
echo "$FACILITATOR_PID" > "$FACILITATOR_PID_FILE"
FACILITATOR_CONTAINER="obol-flow10-x402-facilitator"
if ! start_x402_facilitator_container "$FACILITATOR_CONTAINER" "$FACILITATOR_CONFIG" "$FACILITATOR_LOG"; then
fail "Facilitator container failed to start — inspect $FACILITATOR_LOG"
emit_metrics; exit 0
fi
echo "$FACILITATOR_CONTAINER" > "$FACILITATOR_CONTAINER_FILE"
sleep 3
if curl -sf http://localhost:$FACILITATOR_PORT/supported >/dev/null 2>&1; then
if kill -0 "$FACILITATOR_PID" 2>/dev/null; then
if docker ps --format '{{.Names}}' | grep -qx "$FACILITATOR_CONTAINER"; then
pass "Facilitator started on port $FACILITATOR_PORT"
else
write_x402_facilitator_logs "$FACILITATOR_CONTAINER" "$FACILITATOR_LOG"
fail "Facilitator exited after startup — inspect $FACILITATOR_LOG"
emit_metrics; exit 0
fi
else
fail "Facilitator failed to start (bin: $FACILITATOR_BIN)"
write_x402_facilitator_logs "$FACILITATOR_CONTAINER" "$FACILITATOR_LOG"
fail "Facilitator failed to start (image: $FACILITATOR_IMAGE)"
emit_metrics; exit 0
fi
fi
Expand Down
36 changes: 27 additions & 9 deletions flows/flow-11-dual-stack.sh
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,25 @@ FLOW11_REQUIRED_BOB_USDC=$((FLOW11_BUY_COUNT * FLOW11_PRICE_MICRO_USDC))
mkdir -p "$FLOW11_ARTIFACT_DIR"

# Always reclaim leaked Docker networks on exit so the next run doesn't run
# into "all predefined address pools have been fully subnetted". Each k3d
# cluster create reserves a /16 from Docker's 172.16/12 pool; if a cluster
# crashes mid-create or is force-removed without `obol stack down`, the
# network is orphaned. Targeted to k3d-obol-stack-* and skips networks
# with active endpoints, so it never disturbs a live cluster.
trap cleanup_k3d_obol_networks EXIT
# into "all predefined address pools have been fully subnetted". If the flow
# exits after creating Alice/Bob stacks but before the explicit cleanup section,
# tear those scoped stacks down as well.
flow11_cleanup() {
local ec=$?
set +e
if [ "$ec" -ne 0 ]; then
if type alice >/dev/null 2>&1; then
alice stack down >/dev/null 2>&1 || true
fi
if type bob >/dev/null 2>&1; then
bob stack down >/dev/null 2>&1 || true
fi
fi
cleanup_k3d_obol_networks
set -e
return $ec
}
trap flow11_cleanup EXIT
# Proactive: also reclaim leaked networks at start so the new cluster can
# allocate even if a prior aborted run left orphans behind.
cleanup_k3d_obol_networks
Expand Down Expand Up @@ -290,7 +303,7 @@ except Exception as e:
bob_buy_skill_balance() {
bob kubectl exec \
-n "$BOB_AGENT_NS" "deploy/$BOB_AGENT_DEPLOY" -c "$BOB_AGENT_CONTAINER" -- \
python3 "$BOB_OBOL_SKILLS_DIR/buy-inference/scripts/buy.py" balance 2>&1 || true
python3 "$BOB_OBOL_SKILLS_DIR/buy-x402/scripts/buy.py" balance 2>&1 || true
}

bob_remote_signer_address() {
Expand Down Expand Up @@ -1214,7 +1227,7 @@ buy_response=$(curl -sf --max-time 300 \
\"messages\": [
{\"role\": \"user\", \"content\": \"Search the ERC-8004 registry on Base Sepolia for the agent named 'Dual-Stack Test Inference'. Report its endpoint.\"},
{\"role\": \"assistant\", \"content\": \"I found the agent. Its endpoint is $TUNNEL_URL/services/alice-inference\"},
{\"role\": \"user\", \"content\": \"Now use the buy-inference skill to buy $FLOW11_BUY_COUNT inference tokens from Alice. Run exactly: python3 $BOB_OBOL_SKILLS_DIR/buy-inference/scripts/buy.py buy alice-inference --endpoint $TUNNEL_URL/services/alice-inference/v1/chat/completions --model ${OBOL_LLM_MODEL:-qwen3.5:9b} --count $FLOW11_BUY_COUNT\"}
{\"role\": \"user\", \"content\": \"Now use the buy-x402 skill to buy $FLOW11_BUY_COUNT inference tokens from Alice. Run exactly: python3 $BOB_OBOL_SKILLS_DIR/buy-x402/scripts/buy.py buy alice-inference --endpoint $TUNNEL_URL/services/alice-inference/v1/chat/completions --model ${OBOL_LLM_MODEL:-qwen3.5:9b} --count $FLOW11_BUY_COUNT\"}
],
\"max_tokens\": 4000,
\"stream\": false
Expand All @@ -1225,7 +1238,11 @@ buy_content=$(extract_assistant_content "$buy_response" 2>/dev/null || true)
# structurally by the next step's PurchaseRequest CR Ready=True poll. Natural-language
# matching has been brittle across runtime versions (OpenClaw vs Hermes).
echo "${buy_content:0:500}"
pass "Agent buy command issued (success will be confirmed by PurchaseRequest CR)"
if printf '%s' "$buy_content" | agent_response_refused; then
fail "Agent refused to run buy.py"
emit_metrics; exit 1
fi
pass "Agent accepted buy request (success will be confirmed by PurchaseRequest CR)"

poll_step_grep "Bob: PurchaseRequest Ready" "True" 24 5 purchase_request_status
pr_status=$(purchase_request_status)
Expand Down Expand Up @@ -1411,3 +1428,4 @@ echo " Bob: $BOB_WALLET"
echo " Tunnel: $TUNNEL_URL"
echo " Artifacts: $FLOW11_ARTIFACT_DIR"
echo "════════════════════════════════════════════════════════════"
exit_if_failed
135 changes: 7 additions & 128 deletions flows/flow-12-obol-payment.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,126 +8,9 @@
#
# Requires:
# - A running obol stack with the agent initialized.
# - X402_FACILITATOR_BIN or X402_RS_DIR pointing to a current x402-rs build
# with eip2612GasSponsoring support.
# - Docker access to ghcr.io/x402-rs/x402-facilitator:1.4.7.
source "$(dirname "$0")/lib.sh"

resolve_facilitator_bin() {
if [ -n "${X402_FACILITATOR_BIN:-}" ] && [ -x "$X402_FACILITATOR_BIN" ]; then
if [ -z "${X402_RS_DIR:-}" ]; then
case "$X402_FACILITATOR_BIN" in
*/target/release/*)
X402_RS_DIR=$(cd "$(dirname "$X402_FACILITATOR_BIN")/../.." && pwd)
;;
esac
fi
printf '%s\n' "$X402_FACILITATOR_BIN"
return 0
fi

local rs_dir="${X402_RS_DIR:-}"
if [ -z "$rs_dir" ] && [ -d "$HOME/Development/R&D/x402-rs" ]; then
rs_dir="$HOME/Development/R&D/x402-rs"
fi
if [ -n "$rs_dir" ]; then
for candidate in \
"$rs_dir/target/release/x402-facilitator" \
"$rs_dir/target/release/facilitator"; do
if [ -x "$candidate" ]; then
X402_RS_DIR="$rs_dir"
printf '%s\n' "$candidate"
return 0
fi
done
fi

return 1
}

validate_x402_rs_source() {
local rs_dir="${X402_RS_DIR:-}"
local expect_remote="${FLOW12_EXPECT_X402_RS_REMOTE:-x402-rs/x402-rs}"
local expect_version="${FLOW12_EXPECT_X402_RS_VERSION:-1.4.7}"
local remote version

[ -n "$rs_dir" ] || return 0
[ -d "$rs_dir/.git" ] || return 0

remote=$(git -C "$rs_dir" remote get-url origin 2>/dev/null || true)
if [ -n "$expect_remote" ] && [ "$expect_remote" != "any" ]; then
case "$remote" in
*"$expect_remote"*)
;;
*)
fail "x402-rs origin mismatch: expected remote containing '$expect_remote', got '${remote:-unknown}'"
emit_metrics
exit 1
;;
esac
fi

if [ -n "$expect_version" ] && [ "$expect_version" != "any" ]; then
version=$(python3 - "$rs_dir" <<'PY'
import pathlib
import re
import sys

root = pathlib.Path(sys.argv[1])
workspace_version = ""
root_manifest = root / "Cargo.toml"
if root_manifest.exists():
in_workspace_package = False
for line in root_manifest.read_text().splitlines():
stripped = line.strip()
if stripped == "[workspace.package]":
in_workspace_package = True
continue
if stripped.startswith("[") and stripped != "[workspace.package]":
in_workspace_package = False
if in_workspace_package:
match = re.match(r'version\s*=\s*"([^"]+)"', stripped)
if match:
workspace_version = match.group(1)
break

for path in (root / "facilitator" / "Cargo.toml", root / "crates" / "x402-facilitator-local" / "Cargo.toml"):
if path.exists():
for line in path.read_text().splitlines():
stripped = line.strip()
if re.match(r'version\s*\.workspace\s*=\s*true', stripped) and workspace_version:
print(workspace_version)
raise SystemExit(0)
match = re.match(r'version\s*=\s*"([^"]+)"', stripped)
if match:
print(match.group(1))
raise SystemExit(0)

for path in (root / "crates" / "x402-facilitator" / "Cargo.toml",):
if path.exists():
for line in path.read_text().splitlines():
stripped = line.strip()
match = re.match(r'version\s*=\s*"([^"]+)"', stripped)
if match:
print(match.group(1))
raise SystemExit(0)
if workspace_version:
print(workspace_version)
raise SystemExit(0)
raise SystemExit(1)
PY
) || version=""
if [ "$version" != "$expect_version" ]; then
fail "x402-rs facilitator version mismatch: expected $expect_version, got ${version:-unknown}"
emit_metrics
exit 1
fi
fi

echo " x402-rs origin: ${remote:-unknown}"
echo " x402-rs head: $(git -C "$rs_dir" rev-parse --short HEAD 2>/dev/null || echo unknown)"
echo " x402-rs facilitator version: ${version:-not-checked}"
}

step "local stack context is isolated"
assert_local_stack_context
pass "KUBECONFIG=$KUBECONFIG"
Expand Down Expand Up @@ -165,21 +48,16 @@ else
exit 1
fi

step "x402-rs facilitator binary available for OBOL Permit2"
FACILITATOR_BIN=$(resolve_facilitator_bin || true)
if [ -n "$FACILITATOR_BIN" ]; then
export X402_FACILITATOR_BIN="$FACILITATOR_BIN"
pass "X402_FACILITATOR_BIN=$X402_FACILITATOR_BIN"
step "x402-rs facilitator image available for OBOL Permit2"
FACILITATOR_IMAGE=$(x402_facilitator_image || true)
if [ -n "$FACILITATOR_IMAGE" ]; then
pass "Facilitator image available: $FACILITATOR_IMAGE"
else
fail "x402-rs facilitator binary not found — set X402_FACILITATOR_BIN or X402_RS_DIR"
fail "x402-rs facilitator image unavailable: ghcr.io/x402-rs/x402-facilitator:1.4.7"
emit_metrics
exit 1
fi

step "x402-rs facilitator source matches expected release line"
validate_x402_rs_source
pass "x402-rs facilitator source validated"

step "OBOL Permit2 sell->buy->settle integration test"
ARTIFACT_DIR="${FLOW12_ARTIFACT_DIR:-$OBOL_ROOT/.tmp/flow-12-$(date +%Y%m%d-%H%M%S)}"
mkdir -p "$ARTIFACT_DIR"
Expand Down Expand Up @@ -229,3 +107,4 @@ else
fi

emit_metrics
exit_if_failed
Loading
Loading