diff --git a/.github/workflows/docker-publish-x402-verifier.yml b/.github/workflows/docker-publish-x402.yml similarity index 65% rename from .github/workflows/docker-publish-x402-verifier.yml rename to .github/workflows/docker-publish-x402.yml index 68e7ea1f..a655ed39 100644 --- a/.github/workflows/docker-publish-x402-verifier.yml +++ b/.github/workflows/docker-publish-x402.yml @@ -1,4 +1,4 @@ -name: Build and Publish x402-verifier Image +name: Build and Publish x402 Images on: push: @@ -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 @@ -59,16 +71,16 @@ 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 @@ -76,30 +88,38 @@ jobs: 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' diff --git a/CLAUDE.md b/CLAUDE.md index e0ed19a9..b058c9ab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/internal/embed/infrastructure/base/templates/local-path.yaml b/internal/embed/infrastructure/base/templates/local-path.yaml index 023569f4..e9f6688c 100644 --- a/internal/embed/infrastructure/base/templates/local-path.yaml +++ b/internal/embed/infrastructure/base/templates/local-path.yaml @@ -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 diff --git a/internal/openclaw/openclaw.go b/internal/openclaw/openclaw.go index c5100a91..eb9ee6f8 100644 --- a/internal/openclaw/openclaw.go +++ b/internal/openclaw/openclaw.go @@ -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") } @@ -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 @@ -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 } diff --git a/internal/openclaw/wallet.go b/internal/openclaw/wallet.go index c707f353..ff010833 100644 --- a/internal/openclaw/wallet.go +++ b/internal/openclaw/wallet.go @@ -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 } diff --git a/internal/openclaw/wallet_backup.go b/internal/openclaw/wallet_backup.go index 14f1fc6c..f1440931 100644 --- a/internal/openclaw/wallet_backup.go +++ b/internal/openclaw/wallet_backup.go @@ -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 { diff --git a/obolup.sh b/obolup.sh index 7d3abf13..8fdaa86d 100755 --- a/obolup.sh +++ b/obolup.sh @@ -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" @@ -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 @@ -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 .*?)\\s+depName=(?.*?)\\nreadonly\\s+\\w+=\"(?[^\"]+)\"" + ], + "fileMatch": [ + "^obolup\\.sh$" + ], + "versioningTemplate": "semver", + "extractVersionTemplate": "^v?(?.+)$" + }, + { + "customType": "regex", + "description": "Update k3s image tag in k3d config", + "matchStrings": [ + "image:\\s*rancher/k3s:v(?[\\w.+-]+)" + ], + "fileMatch": [ + "^internal/embed/k3d-config\\.yaml$" + ], + "datasourceTemplate": "github-releases", + "depNameTemplate": "k3s-io/k3s", + "versioningTemplate": "regex:^v?(?\\d+)\\.(?\\d+)\\.(?\\d+)([+-](?.+))?$", + "extractVersionTemplate": "^v(?.+)$" } ], "packageRules": [ @@ -172,6 +198,48 @@ "before 6am on monday" ], "groupName": "LiteLLM updates" + }, + { + "description": "Batch obolup.sh tool version updates into a single PR", + "matchFileNames": [ + "obolup.sh" + ], + "matchPackageNames": [ + "kubernetes/kubernetes", + "helm/helm", + "k3d-io/k3d", + "helmfile/helmfile", + "derailed/k9s", + "databus23/helm-diff", + "ollama/ollama" + ], + "labels": [ + "renovate/obolup-deps" + ], + "schedule": [ + "before 6am on monday" + ], + "groupName": "obolup.sh dependency updates" + }, + { + "description": "Limit Helm to 3.x (Helm 4 is a breaking major)", + "matchPackageNames": [ + "helm/helm" + ], + "allowedVersions": "<4.0.0" + }, + { + "description": "Group k3s image updates", + "matchPackageNames": [ + "k3s-io/k3s" + ], + "labels": [ + "renovate/k3s" + ], + "schedule": [ + "before 6am on monday" + ], + "groupName": "k3s image updates" } ] }