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
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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ The Cloudflare tunnel exposes the cluster to the public internet. Only x402-gate

**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.0, helm 3.19.4, k3d 5.8.3, helmfile 1.2.3, k9s 0.50.18, helm-diff 3.14.1. Key Go: `urfave/cli/v3`, `dustinkirkland/golang-petname`, `mark3labs/x402-go`.
**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
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
44 changes: 44 additions & 0 deletions internal/openclaw/openclaw.go
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,7 @@ func copyWorkspaceToVolume(cfg *config.Config, id, workspaceDir string, u *ui.UI
return
}

fixVolumeOwnership(cfg, targetDir)
u.Success("Imported workspace to volume")
}

Expand Down Expand Up @@ -673,6 +674,48 @@ func injectSkillsToVolume(cfg *config.Config, id string, deploymentDir string, u
}
u.Successf("Injected skill: %s", e.Name())
}

fixVolumeOwnership(cfg, targetDir)
}

// fixVolumeOwnership normalises file ownership on a host-side PVC path so the
// container (UID 1000 / node) can read and write. On k3d the host path is
// inside a Docker container (the k3d node), so we exec into it as root and
// chown recursively. On k3s the host IS the node, so we attempt a direct
// chown (works when the CLI runs as root, harmless no-op otherwise).
func fixVolumeOwnership(cfg *config.Config, hostPath string) {
// Determine backend (default: k3d for backward compat).
backendName := "k3d"
if data, err := os.ReadFile(filepath.Join(cfg.ConfigDir, ".stack-backend")); err == nil {
backendName = strings.TrimSpace(string(data))
}

switch backendName {
case "k3d":
stackID := ""
if data, err := os.ReadFile(filepath.Join(cfg.ConfigDir, ".stack-id")); err == nil {
stackID = strings.TrimSpace(string(data))
}
if stackID == "" {
return
}
container := fmt.Sprintf("k3d-obol-stack-%s-server-0", stackID)

// Convert host path to the in-node path. k3d mounts $DATA_DIR → /data.
relPath, err := filepath.Rel(cfg.DataDir, hostPath)
if err != nil {
return
}
nodePath := filepath.Join("/data", relPath)

cmd := exec.Command("docker", "exec", container,
"chown", "-R", "1000:1000", nodePath)
_ = cmd.Run() // best-effort
default:
// k3s — direct host, try chown (succeeds if root).
cmd := exec.Command("chown", "-R", "1000:1000", hostPath)
_ = cmd.Run()
}
}

// copyDirRecursive copies a directory tree from src to dst, creating
Expand Down Expand Up @@ -1279,6 +1322,7 @@ func SkillsSync(cfg *config.Config, id, skillsDir string, u *ui.UI) error {
u.Successf("Synced skill: %s", e.Name())
}

fixVolumeOwnership(cfg, targetDir)
u.Success("Skills synced to volume (file watcher will reload)")
return nil
}
Expand Down
1 change: 1 addition & 0 deletions internal/openclaw/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ func provisionKeystoreToVolume(cfg *config.Config, id, keystoreID string, keysto
return "", fmt.Errorf("write keystore: %w", err)
}

fixVolumeOwnership(cfg, dir)
return path, nil
}

Expand Down
1 change: 1 addition & 0 deletions internal/openclaw/wallet_backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ func RestoreWalletCmd(cfg *config.Config, id string, opts RestoreWalletOptions,
if err := os.WriteFile(keystorePath, []byte(w.Keystore), 0600); err != nil {
return fmt.Errorf("failed to write keystore: %w", err)
}
fixVolumeOwnership(cfg, keystoreDir)

// Update values-remote-signer.yaml with restored password.
if err := writeKeystorePassword(deployDir, w.KeystorePassword); err != nil {
Expand Down
122 changes: 108 additions & 14 deletions obolup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,20 @@ fi

# Pinned dependency versions
# Update these versions to upgrade dependencies across all installations
readonly KUBECTL_VERSION="1.35.0"
readonly HELM_VERSION="3.19.4"
# renovate: datasource=github-releases depName=kubernetes/kubernetes
readonly KUBECTL_VERSION="1.35.3"
# renovate: datasource=github-releases depName=helm/helm
readonly HELM_VERSION="3.20.1"
# renovate: datasource=github-releases depName=k3d-io/k3d
readonly K3D_VERSION="5.8.3"
readonly HELMFILE_VERSION="1.2.3"
# renovate: datasource=github-releases depName=helmfile/helmfile
readonly HELMFILE_VERSION="1.4.3"
# renovate: datasource=github-releases depName=derailed/k9s
readonly K9S_VERSION="0.50.18"
readonly HELM_DIFF_VERSION="3.14.1"
# renovate: datasource=github-releases depName=databus23/helm-diff
readonly HELM_DIFF_VERSION="3.15.4"
# renovate: datasource=github-releases depName=ollama/ollama
readonly OLLAMA_VERSION="0.20.2"
# Must match internal/openclaw/OPENCLAW_VERSION (without "v" prefix).
# Tested by TestOpenClawVersionConsistency.
readonly OPENCLAW_VERSION="2026.3.24"
Expand Down Expand Up @@ -1170,18 +1178,50 @@ WRAPPER
return 1
}

# Configure Ollama to listen on 0.0.0.0 so k3d containers can reach it.
# On macOS, Docker Desktop routes host.docker.internal through the hypervisor,
# so Ollama's bind address doesn't matter. On Linux, k3d uses the docker
# bridge network, so Ollama must accept connections from the bridge gateway IP.
configure_ollama_host_binding() {
# Only needed on Linux with systemd
[[ "$(uname -s)" != "Linux" ]] && return 0
command_exists systemctl || return 0
systemctl list-unit-files ollama.service >/dev/null 2>&1 || return 0

local override_dir="/etc/systemd/system/ollama.service.d"
local override_file="$override_dir/obol-host.conf"

# Skip if already configured
if [[ -f "$override_file" ]]; then
return 0
fi

log_info "Configuring Ollama to listen on 0.0.0.0 (required for k3d)..."

if sudo mkdir -p "$override_dir" && \
sudo tee "$override_file" >/dev/null <<'OVERRIDE'
[Service]
Environment="OLLAMA_HOST=0.0.0.0"
OVERRIDE
then
sudo systemctl daemon-reload
sudo systemctl restart ollama 2>/dev/null || true
log_success "Ollama configured to listen on all interfaces"
else
log_warn "Could not configure Ollama host binding (non-fatal)"
echo " Manually set OLLAMA_HOST=0.0.0.0 in your Ollama config"
fi
}

# Install Ollama (host runtime for local AI inference)
# Unlike other dependencies, Ollama is a full application with a background server.
# On macOS it installs Ollama.app; on Linux it sets up a systemd service.
# We delegate to Ollama's official installer rather than downloading a binary ourselves.
install_ollama() {
# Check for existing ollama installation
if command_exists ollama; then
local version
version=$(ollama --version 2>/dev/null | sed 's/ollama version is //' || echo "unknown")
log_success "Ollama v$version already installed"
local target_version="$OLLAMA_VERSION"

# Check if the server is running
# Helper: check if server is running and print status
check_ollama_server() {
if curl -sf http://localhost:11434/api/tags >/dev/null 2>&1; then
log_success "Ollama server is running"
else
Expand All @@ -1197,7 +1237,60 @@ install_ollama() {
esac
echo ""
fi
return 0
}

# Check for existing ollama installation
if command_exists ollama; then
local version
version=$(ollama --version 2>/dev/null | sed 's/ollama version is //' || echo "unknown")

# Check if upgrade is needed
if [[ "$version" != "unknown" ]] && version_ge "$version" "$target_version"; then
log_success "Ollama v$version is up to date"
configure_ollama_host_binding
check_ollama_server
return 0
fi

if [[ "$version" != "unknown" ]]; then
log_warn "Ollama v$version is older than pinned v$target_version"

# Prompt for upgrade if interactive
if [[ -c /dev/tty ]]; then
local choice
read -p " Upgrade Ollama to v$target_version? [Y/n]: " choice </dev/tty

case "$choice" in
[Nn]*)
log_warn "Skipping Ollama upgrade"
check_ollama_server
return 0
;;
esac
else
log_warn "Non-interactive — skipping Ollama upgrade"
check_ollama_server
return 0
fi

log_info "Upgrading Ollama to v$target_version..."
echo ""
if env OLLAMA_VERSION="$target_version" bash -c 'curl -fsSL https://ollama.com/install.sh | sh'; then
echo ""
log_success "Ollama upgraded to v$target_version"
configure_ollama_host_binding
check_ollama_server
return 0
else
log_warn "Ollama upgrade failed — continuing with v$version"
check_ollama_server
return 0
fi
else
log_success "Ollama already installed (version unknown)"
check_ollama_server
return 0
fi
fi

# Ollama not found — ask user if they want to install it
Expand Down Expand Up @@ -1225,11 +1318,12 @@ install_ollama() {
;;
*)
# Yes — delegate to official installer
log_info "Installing Ollama..."
log_info "Installing Ollama v$target_version..."
echo ""
if curl -fsSL https://ollama.com/install.sh | sh; then
if env OLLAMA_VERSION="$target_version" bash -c 'curl -fsSL https://ollama.com/install.sh | sh'; then
echo ""
log_success "Ollama installed"
log_success "Ollama v$target_version installed"
configure_ollama_host_binding

# On macOS, the installer starts Ollama.app automatically.
# On Linux with systemd, the installer enables and starts the service.
Expand Down
Loading
Loading