Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Build and Publish x402-verifier Image
name: Build and Publish x402 Images

on:
push:
Expand All @@ -10,33 +10,45 @@ on:
paths:
- 'internal/x402/**'
- 'cmd/x402-verifier/**'
- 'cmd/x402-buyer/**'
- 'Dockerfile.x402-verifier'
- 'Dockerfile.x402-buyer'
- 'go.mod'
- 'go.sum'
- '.github/workflows/docker-publish-x402-verifier.yml'
- '.github/workflows/docker-publish-x402.yml'
workflow_dispatch:

concurrency:
group: x402-verifier-${{ github.ref }}
group: x402-${{ github.ref }}
cancel-in-progress: true


env:
REGISTRY: ghcr.io
IMAGE_NAME: obolnetwork/x402-verifier

jobs:
# ---------------------------------------------------------------------------
# Job 1: Build the x402-verifier binary and publish the image.
# Uses the same action versions as the working OpenClaw workflow.
# Build each x402 component and publish its image.
# ---------------------------------------------------------------------------
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- component: x402-verifier
image: obolnetwork/x402-verifier
dockerfile: Dockerfile.x402-verifier
description: x402 payment verification sidecar for Obol Stack
- component: x402-buyer
image: obolnetwork/x402-buyer
dockerfile: Dockerfile.x402-buyer
description: x402 buy-side payment sidecar for Obol Stack
outputs:
digest: ${{ steps.build-push.outputs.digest }}
verifier-digest: ${{ steps.build-push.outputs.digest }}
buyer-digest: ${{ steps.build-push.outputs.digest }}

steps:
- name: Checkout
Expand All @@ -59,47 +71,55 @@ jobs:
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
images: ${{ env.REGISTRY }}/${{ matrix.image }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/feat/secure-enclave-inference' }}
labels: |
org.opencontainers.image.title=x402-verifier
org.opencontainers.image.description=x402 payment verification sidecar for Obol Stack
org.opencontainers.image.vendor=Obol Network
org.opencontainers.image.title=${{ matrix.component }}
org.opencontainers.image.description=${{ matrix.description }}
org.opencontainers.image.vendor=Obol
org.opencontainers.image.source=https://github.com/ObolNetwork/obol-stack

- name: Build and push
id: build-push
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
file: Dockerfile.x402-verifier
file: ${{ matrix.dockerfile }}
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=x402-verifier
cache-to: type=gha,scope=x402-verifier,mode=max
cache-from: type=gha,scope=${{ matrix.component }}
cache-to: type=gha,scope=${{ matrix.component }},mode=max
provenance: true
sbom: true

# ---------------------------------------------------------------------------
# Job 2: Security scan the published image using the exact digest from build.
# Security scan each published image.
# ---------------------------------------------------------------------------
security-scan:
needs: build
runs-on: ubuntu-latest
permissions:
security-events: write
strategy:
fail-fast: false
matrix:
include:
- component: x402-verifier
image: obolnetwork/x402-verifier
- component: x402-buyer
image: obolnetwork/x402-buyer

steps:
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build.outputs.digest }}
image-ref: ${{ env.REGISTRY }}/${{ matrix.image }}:latest
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
Expand Down
24 changes: 23 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,29 @@ The Cloudflare tunnel exposes the cluster to the public internet. Only x402-gate

## Dependencies

Docker 20.10.0+, Go 1.25+. Toolchain installed by `obolup.sh` (kubectl, helm, k3d, helmfile, k9s). Key Go deps: `urfave/cli/v3`, `dustinkirkland/golang-petname`, `mark3labs/x402-go`. E2E monetize walkthrough: `@docs/guides/monetize-inference.md`.
| Package | Key Files | Role |
|---------|-----------|------|
| `cmd/obol` | `main.go`, `sell.go`, `network.go`, `openclaw.go`, `model.go` | CLI commands |
| `internal/config` | `config.go` | XDG Config struct |
| `internal/stack` | `stack.go` | Cluster lifecycle |
| `internal/network` | `network.go`, `erpc.go`, `rpc.go`, `parser.go` | Networks, eRPC, RPC gateway |
| `internal/x402` | `config.go`, `setup.go`, `verifier.go`, `matcher.go`, `watcher.go` | ForwardAuth verifier |
| `internal/x402/buyer` | `signer.go`, `proxy.go`, `config.go` | Buy-side sidecar |
| `internal/erc8004` | `client.go`, `types.go`, `abi.go` | ERC-8004 Identity Registry |
| `internal/agent` | `agent.go` | obol-agent singleton, RBAC patching |
| `internal/model` | `model.go` | LiteLLM gateway configuration |
| `internal/openclaw` | `openclaw.go`, `wallet.go`, `resolve.go` | OpenClaw setup, wallet, instance resolution |
| `internal/inference` | `gateway.go`, `container.go`, `store.go` | Standalone x402 gateway |
| `internal/enclave` | `enclave.go`, `enclave_darwin.go`, `enclave_stub.go` | Secure Enclave keys |
| `internal/embed` | `embed.go` | Embedded assets (skills, infrastructure, networks) |

**Embedded assets**: `internal/embed/infrastructure/` (K8s templates), `internal/embed/networks/` (ethereum, helios, aztec), `internal/embed/skills/` (23 skills).

**Tests**: `cmd/obol/sell_test.go` (CLI flags), `internal/x402/*_test.go` (verifier, config, matcher, E2E), `internal/erc8004/*_test.go` (ABI, client), `internal/embed/embed_crd_test.go` (CRD+RBAC validation), `internal/openclaw/integration_test.go` (full-cluster inference), `internal/openclaw/overlay_test.go`, `internal/inference/gateway_test.go`.

**Docs**: `docs/guides/monetize-inference.md` (E2E monetize walkthrough), `README.md`.

**Deps**: Docker 20.10.0+, Go 1.25+. Installed by obolup.sh: kubectl 1.35.3, helm 3.20.1, k3d 5.8.3, helmfile 1.4.3, k9s 0.50.18, helm-diff 3.15.4, ollama 0.20.2. Key Go: `urfave/cli/v3`, `dustinkirkland/golang-petname`, `mark3labs/x402-go`.

## Related Codebases

Expand Down
2 changes: 1 addition & 1 deletion cmd/obol/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func bootstrapCommand(cfg *config.Config) *cli.Command {
}

// Step 2: Start stack
if err := stack.Up(cfg, u); err != nil {
if err := stack.Up(cfg, u, false); err != nil {
return fmt.Errorf("bootstrap up failed: %w", err)
}

Expand Down
8 changes: 7 additions & 1 deletion cmd/obol/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,14 @@ GLOBAL OPTIONS:{{template "visibleFlagTemplate" .}}{{end}}
{
Name: "up",
Usage: "Start the Obol Stack",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "wildcard-dns",
Usage: "Configure wildcard *.obol.stack DNS via NetworkManager/dnsmasq (Linux) or /etc/resolver (macOS)",
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
return stack.Up(cfg, getUI(cmd))
return stack.Up(cfg, getUI(cmd), cmd.Bool("wildcard-dns"))
},
},
{
Expand Down
45 changes: 44 additions & 1 deletion cmd/obol/sell.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,49 @@ Examples:
return err
}

// Auto-detect model and upstream if --model not specified.
modelFlag := cmd.String("model")
upstreamFlag := cmd.String("upstream")
if modelFlag == "" && u.IsTTY() {
detected, scanErr := inference.ScanLocalEndpointsContext(ctx)
if scanErr == nil && len(detected) > 0 {
fmt.Println("\nDetected local inference servers:")
fmt.Print(inference.FormatEndpointDisplay(detected))

type pick struct {
baseURL, modelID string
}
var picks []pick
idx := 1
for _, ep := range detected {
for _, m := range ep.Models {
fmt.Printf(" [%d] %s — %s (%s)\n", idx, m.ID, ep.BaseURL(), ep.ServerType)
picks = append(picks, pick{ep.BaseURL(), m.ID})
idx++
}
}

if len(picks) == 1 {
answer, _ := u.Input(fmt.Sprintf("Use %s on %s? [Y/n]", picks[0].modelID, picks[0].baseURL), "")
answer = strings.TrimSpace(strings.ToLower(answer))
if answer == "" || answer == "y" || answer == "yes" {
modelFlag = picks[0].modelID
upstreamFlag = picks[0].baseURL
}
} else if len(picks) > 1 {
sel, _ := u.Input("Select [1]", "1")
n, parseErr := strconv.Atoi(strings.TrimSpace(sel))
if parseErr == nil && n >= 1 && n <= len(picks) {
modelFlag = picks[n-1].modelID
upstreamFlag = picks[n-1].baseURL
}
}
}
}
if modelFlag == "" {
return fmt.Errorf("--model is required (or run interactively to auto-detect)")
}

teeType := cmd.String("tee")
modelHash := cmd.String("model-hash")

Expand Down Expand Up @@ -233,7 +276,7 @@ Examples:
Name: name,
EnclaveTag: cmd.String("enclave-tag"),
ListenAddr: cmd.String("listen"),
UpstreamURL: cmd.String("upstream"),
UpstreamURL: upstreamFlag,
WalletAddress: wallet,
PricePerRequest: perRequest,
PricePerMTok: priceTable.PerMTok,
Expand Down
19 changes: 4 additions & 15 deletions internal/dns/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
package dns

import (
"errors"
"fmt"
"os"
"os/exec"
Expand Down Expand Up @@ -349,20 +348,15 @@ func removeMacOSResolver() {
// --- Linux (NetworkManager dnsmasq plugin) ---

// configureLinuxResolver sets up NM's dnsmasq plugin for *.obol.stack.
// Returns a non-fatal error when NetworkManager is unavailable (e.g. headless
// servers) — the caller falls back to /etc/hosts entries.
func configureLinuxResolver() error {
if configureNMDnsmasq() {
return nil
}

// NM not available — print instructions
fmt.Println("\nWildcard DNS for *.obol.stack requires NetworkManager with dnsmasq.")
fmt.Println("Install with:")
fmt.Println(" sudo apt install network-manager dnsmasq-base # Debian/Ubuntu")
fmt.Println(" sudo dnf install NetworkManager dnsmasq # Fedora/RHEL")
fmt.Println(" sudo pacman -S networkmanager dnsmasq # Arch")
fmt.Println("\nThen re-run: obol stack up")

return errors.New("NetworkManager required for wildcard DNS on Linux")
// NM not available — return quiet error; caller handles the fallback message.
return fmt.Errorf("NetworkManager not available (headless or server system)")
}

// hasNMDnsmasqConfig checks if the NM dnsmasq config for obol.stack exists.
Expand Down Expand Up @@ -394,11 +388,6 @@ func configureNMDnsmasq() bool {

// Check if dnsmasq binary is available (NM plugin requires it)
if _, err := exec.LookPath("dnsmasq"); err != nil {
fmt.Println("Note: dnsmasq not found. Install it for wildcard DNS support:")
fmt.Println(" sudo apt install dnsmasq-base # Debian/Ubuntu")
fmt.Println(" sudo dnf install dnsmasq # Fedora/RHEL")
fmt.Println(" sudo pacman -S dnsmasq # Arch")

return false
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ data:
#!/bin/sh
set -eu
mkdir -m 0755 -p "${VOL_DIR}"
chown 1000:1000 "${VOL_DIR}"
chown -R 1000:1000 "${VOL_DIR}"
teardown: |-
#!/bin/sh
set -eu
Expand Down
Loading