Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
2d1ba04
chore: upgrade pinned dependency versions in obolup.sh
bussyjd Jan 12, 2026
d110104
feat: replace nginx-ingress with Traefik and Gateway API
bussyjd Jan 13, 2026
ba54ea5
feat: add monitoring stack and gateway updates
bussyjd Jan 14, 2026
ccfef55
feat: add cloudflared tunnel for public service exposure
bussyjd Jan 14, 2026
bd21826
docs: update CLAUDE.md with new dependency versions
bussyjd Jan 27, 2026
51e495d
Merge PR #123
bussyjd Feb 3, 2026
1f03040
Merge PR #127
bussyjd Feb 3, 2026
1ed55b0
Merge PR #128
bussyjd Feb 3, 2026
c3ad1b9
Merge PR #129
bussyjd Feb 3, 2026
d5e5ccd
feat(auth): add dashboard auth and nodecore token refresh
bussyjd Feb 2, 2026
09356aa
feat(llm): add ollama cloud + llmspy foundation
bussyjd Feb 3, 2026
5328fc6
docs: note llmspy + ollama cloud default
bussyjd Feb 3, 2026
9e4b885
chore(llm): use official llmspy image and tcp probes
bussyjd Feb 3, 2026
8e8767b
docs(okr1): note official llmspy image
bussyjd Feb 3, 2026
9b98def
fix(llm): run llmspy via llms entrypoint
bussyjd Feb 3, 2026
8798d07
fix(llm): use http probes for llmspy
bussyjd Feb 3, 2026
37ed241
feat: persist Cloudflare Tunnel hostname via login + loosen Gateway h…
bussyjd Feb 4, 2026
0de01e5
chore: bump cloudflared to 2026.1.2
bussyjd Feb 5, 2026
8d97179
Merge branch 'codex/persistent-tunnel-url' into integration-okr-1
bussyjd Feb 5, 2026
1f71012
feat(inference): add x402 pay-per-inference gateway (Phase 1)
bussyjd Feb 6, 2026
7d0f58f
Merge pull request #136 from ObolNetwork/feat/x402-inference-gateway
bussyjd Feb 8, 2026
b5564fc
fix(infra): fix helmfile template errors in defaults deployment
bussyjd Feb 8, 2026
6c4cbc6
refactor(llm): remove in-cluster Ollama, proxy to host via ExternalName
bussyjd Feb 9, 2026
c3997f1
Merge pull request #140 from ObolNetwork/fix/llmspy-host-routing
bussyjd Feb 9, 2026
fa40287
fix(infra): disable obol-agent from default stack deployment
bussyjd Feb 9, 2026
79d4b99
Merge pull request #141 from ObolNetwork/fix/disable-obol-agent
bussyjd Feb 9, 2026
a2718de
ci(openclaw): add Docker image build workflow with Renovate auto-bump
bussyjd Feb 9, 2026
bf4039f
ci(openclaw): temporarily add test branches to workflow triggers
bussyjd Feb 10, 2026
104c03b
ci(openclaw): trigger workflow test run
bussyjd Feb 10, 2026
2fa8ae7
fix(ci): update Trivy and CodeQL action SHAs to latest
bussyjd Feb 10, 2026
13f84ca
ci(openclaw): re-trigger workflow to verify security scan fix
bussyjd Feb 10, 2026
e27de58
chore(openclaw): bump version to v2026.2.9
bussyjd Feb 10, 2026
ada01b8
Merge pull request #143 from ObolNetwork/feat/openclaw-ci
bussyjd Feb 10, 2026
8bd173d
feat(openclaw): add OpenClaw CLI and Helm chart (#137)
bussyjd Feb 12, 2026
e9b2b09
fix(openclaw): update model defaults and improve chart documentation
bussyjd Feb 12, 2026
65b138a
fix(openclaw): sync chart hardening from helm-charts
bussyjd Feb 12, 2026
85b2c4b
feat(dns): add wildcard DNS resolver for *.obol.stack
bussyjd Feb 12, 2026
e2d3dc1
feat(dns): add Linux support and fix llmspy image tag
bussyjd Feb 12, 2026
ca835f5
refactor(openclaw): replace embedded chart with remote obol/openclaw …
bussyjd Feb 13, 2026
532b23d
cleanup(network): remove Helios light client network (#146)
bussyjd Feb 13, 2026
0bdbafe
test(openclaw): add import pipeline tests and fix silent failures (#147)
bussyjd Feb 13, 2026
22b05a9
fix(openclaw): add controlUi gateway settings for Traefik HTTP proxy …
bussyjd Feb 13, 2026
5e6c751
fix(openclaw): rename virtual provider from "ollama" to "llmspy" for …
bussyjd Feb 12, 2026
1fd4b88
Merge pull request #149 from ObolNetwork/fix/openclaw-llmspy-provider
bussyjd Feb 13, 2026
817dc5f
security(openclaw): remove dangerouslyDisableDeviceAuth, keep only al…
bussyjd Feb 13, 2026
48ae09a
chore(llm): bump LLMSpy image to 3.0.32-obol.1-rc.4
bussyjd Feb 13, 2026
e9d3624
Merge integration-okr-1 into feat/wildcard-dns-resolver
bussyjd Feb 13, 2026
5cba2f4
Merge pull request #151 from ObolNetwork/feat/wildcard-dns-resolver
bussyjd Feb 13, 2026
44f55f7
security(openclaw): stop logging sensitive APIKey field value in import
bussyjd Feb 13, 2026
1c65a2e
Merge main into integration-okr-1
bussyjd Feb 13, 2026
0fae81c
feat(erpc): switch upstream from nodecore to erpc.gcp.obol.tech
bussyjd Feb 13, 2026
8535884
Merge pull request #159 from ObolNetwork/feat/erpc-gcp-upstream
bussyjd Feb 13, 2026
5dfbe7a
Merge integration-okr-1 into fix/openclaw-llmspy-provider
bussyjd Feb 13, 2026
058164a
Merge pull request #157 from ObolNetwork/fix/openclaw-llmspy-provider
bussyjd Feb 13, 2026
7a003b0
chore: switch default model from glm-4.7-flash to gpt-oss:120b-cloud
bussyjd Feb 13, 2026
14901de
fix(openclaw): revert llmspy provider name to ollama for chart compat…
bussyjd Feb 13, 2026
64ef177
fix(llm): use llmspy image for init container with provider merge script
bussyjd Feb 13, 2026
5ab1268
fix(obolup): auto-start Docker daemon on Linux (snap + systemd)
bussyjd Feb 13, 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
119 changes: 119 additions & 0 deletions .github/workflows/docker-publish-openclaw.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
name: Build and Publish OpenClaw Image

on:
push:
branches:
- main
- integration-okr-1 # TODO: remove after testing — limit to main only
- feat/openclaw-ci # TODO: remove after testing — limit to main only
paths:
- 'internal/openclaw/OPENCLAW_VERSION'
workflow_dispatch:
inputs:
version:
description: 'OpenClaw version to build (e.g. v2026.2.3)'
required: false
type: string

env:
REGISTRY: ghcr.io
IMAGE_NAME: obolnetwork/openclaw

jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout obol-stack
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0

- name: Read pinned version
id: version
run: |
if [ -n "${{ github.event.inputs.version }}" ]; then
VERSION="${{ github.event.inputs.version }}"
else
VERSION=$(grep -v '^#' internal/openclaw/OPENCLAW_VERSION | tr -d '[:space:]')
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Building OpenClaw $VERSION"

- name: Checkout upstream OpenClaw
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
repository: openclaw/openclaw
ref: ${{ steps.version.outputs.version }}
path: openclaw-src

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1

- name: Set up QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0

- name: Login to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}},value=${{ steps.version.outputs.version }}
type=semver,pattern={{major}}.{{minor}},value=${{ steps.version.outputs.version }}
type=sha,prefix=
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
labels: |
org.opencontainers.image.title=OpenClaw
org.opencontainers.image.description=AI agent gateway for Obol Stack
org.opencontainers.image.vendor=Obol Network
org.opencontainers.image.source=https://github.com/openclaw/openclaw
org.opencontainers.image.version=${{ steps.version.outputs.version }}

- name: Build and push Docker image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: openclaw-src
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: true
sbom: true

security-scan:
needs: build-and-push
runs-on: ubuntu-latest
permissions:
security-events: write

steps:
- name: Read pinned version
id: version
run: |
# Re-derive for the scan job
echo "Scanning latest pushed image"

- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@22438a435773de8c97dc0958cc0b823c45b064ac # master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'

- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@b13d724d35ff0a814e21683638ed68ed34cf53d1 # main
with:
sarif_file: 'trivy-results.sarif'
if: always()
153 changes: 153 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ The Obol Stack is a local Kubernetes-based framework for running blockchain netw
5. **Two-stage templating**: CLI flags → Go templates → Helmfile → Kubernetes resources
6. **Development mode**: Local `.workspace/` directory with `go run` wrapper for rapid development

### Routing and Gateway API

Obol Stack uses Traefik with the Kubernetes Gateway API for HTTP routing.

- Controller: Traefik Helm chart (`traefik` namespace)
- GatewayClass: `traefik`
- Gateway: `traefik-gateway` in `traefik` namespace
- HTTPRoute patterns:
- `/` → `obol-frontend`
- `/rpc` → `erpc`
- `/ethereum-<id>/execution` and `/ethereum-<id>/beacon`
- `/aztec-<id>` and `/helios-<id>`

## Bootstrap Installer: obolup.sh

### Purpose
Expand Down Expand Up @@ -135,6 +148,12 @@ obol
│ │ ├── helios (dynamically generated)
│ │ └── aztec (dynamically generated)
│ └── delete
├── llm (LLM provider management)
│ └── configure
├── openclaw (OpenClaw AI assistant)
│ ├── setup
│ ├── onboard
│ └── dashboard
├── kubectl (passthrough with KUBECONFIG)
├── helm (passthrough with KUBECONFIG)
├── helmfile (passthrough with KUBECONFIG)
Expand Down Expand Up @@ -547,6 +566,122 @@ obol network install ethereum --id hoodi-test --network=hoodi
- k3s auto-applies all YAML files on startup
- Uses k3s HelmChart CRD for Helm deployments

## LLM Configuration Architecture

The stack uses a two-tier architecture for LLM routing. A cluster-wide proxy (llmspy) handles actual provider communication, while each application instance (e.g., OpenClaw) sees a simplified single-provider view.

### Tier 1: Global llmspy Gateway (`llm` namespace)

**Purpose**: Shared OpenAI-compatible proxy that routes LLM traffic from all applications to actual providers (Ollama, Anthropic, OpenAI).

**Kubernetes resources** (defined in `internal/embed/infrastructure/base/templates/llm.yaml`):

| Resource | Type | Purpose |
|----------|------|---------|
| `llm` | Namespace | Dedicated namespace for LLM infrastructure |
| `llmspy-config` | ConfigMap | `llms.json` (provider enable/disable) + `providers.json` (provider definitions) |
| `llms-secrets` | Secret | Cloud API keys (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`) — empty by default |
| `llmspy` | Deployment | `ghcr.io/obolnetwork/llms:3.0.32-obol.1-rc.1`, port 8000 |
| `llmspy` | Service (ClusterIP) | `llmspy.llm.svc.cluster.local:8000` |
| `ollama` | Service (ExternalName) | Routes to host Ollama via `{{OLLAMA_HOST}}` placeholder |

**Configuration mechanism** (`internal/llm/llm.go` — `ConfigureLLMSpy()`):
1. Patches `llms-secrets` Secret with the API key
2. Reads `llmspy-config` ConfigMap, sets `providers.<name>.enabled = true` in `llms.json`
3. Restarts `llmspy` Deployment via rollout restart
4. Waits for rollout to complete (60s timeout)

**CLI surface** (`cmd/obol/llm.go`):
- `obol llm configure --provider=anthropic --api-key=sk-...`
- Interactive prompt if flags omitted (choice of Anthropic or OpenAI)

**Key design**: Ollama is enabled by default; cloud providers are disabled until configured via `obol llm configure`. An init container copies the ConfigMap into a writable emptyDir so llmspy can write runtime state.

### Tier 2: Per-Instance Application Config (per-deployment namespace)

**Purpose**: Each application instance (e.g., OpenClaw) has its own model configuration, rendered by its Helm chart from values files.

**Values file hierarchy** (helmfile merges in order):
1. `values.yaml` — chart defaults (from embedded chart, e.g., `internal/openclaw/chart/values.yaml`)
2. `values-obol.yaml` — Obol Stack overlay (generated by `generateOverlayValues()`)

**How providers become application config** (OpenClaw example, `_helpers.tpl` lines 167-189):
- Iterates provider list from `.Values.models`
- Only emits providers where `enabled == true`
- For each enabled provider: `baseUrl`, `apiKey` (as `${ENV_VAR}` reference), `models` array
- `api` field is only emitted if non-empty (required for llmspy routing)

### The llmspy-Routed Overlay Pattern

When a cloud provider is selected during setup, two things happen simultaneously:

1. **Global tier**: `llm.ConfigureLLMSpy()` patches the cluster-wide llmspy gateway with the API key and enables the provider
2. **Instance tier**: `buildLLMSpyRoutedOverlay()` creates an overlay where a "llmspy" provider points at the llmspy gateway, the cloud model is listed under that provider with a `llmspy/` prefix, and `api` is set to `openai-completions`. The default "ollama" provider is disabled.

**Result**: The application never talks directly to cloud APIs. All traffic is routed through llmspy.

**Data flow**:
```
Application (openclaw.json)
│ model: "llmspy/claude-sonnet-4-5-20250929"
│ api: "openai-completions"
│ baseUrl: http://llmspy.llm.svc.cluster.local:8000/v1
llmspy (llm namespace, port 8000)
│ POST /v1/chat/completions
│ → resolves "claude-sonnet-4-5-20250929" to anthropic provider
Anthropic API (or Ollama, OpenAI — depending on provider)
```

**Overlay example** (`values-obol.yaml` for cloud provider path):
```yaml
models:
llmspy:
enabled: true
baseUrl: http://llmspy.llm.svc.cluster.local:8000/v1
api: openai-completions
apiKeyEnvVar: LLMSPY_API_KEY
apiKeyValue: llmspy-default
models:
- id: claude-sonnet-4-5-20250929
name: Claude Sonnet 4.5
ollama:
enabled: false
anthropic:
enabled: false
openai:
enabled: false
```

**Note**: The default Ollama path (no cloud provider) still uses the "ollama" provider name pointing at llmspy, since it genuinely routes Ollama model traffic.

### Summary Table

| Aspect | Tier 1 (llmspy) | Tier 2 (Application instance) |
|--------|-----------------|-------------------------------|
| **Scope** | Cluster-wide | Per-deployment |
| **Namespace** | `llm` | `<app>-<id>` (e.g., `openclaw-<id>`) |
| **Config storage** | ConfigMap `llmspy-config` | ConfigMap `<release>-config` |
| **Secrets** | Secret `llms-secrets` | Secret `<release>-secrets` |
| **Configure via** | `obol llm configure` | `obol openclaw setup <id>` |
| **Providers** | Real (Ollama, Anthropic, OpenAI) | Cloud: "llmspy" virtual provider; Default: "ollama" pointing at llmspy |
| **API field** | N/A (provider-native) | Must be `openai-completions` for llmspy routing |

### Key Source Files

| File | Role |
|------|------|
| `internal/llm/llm.go` | `ConfigureLLMSpy()` — patches global Secret + ConfigMap + restart |
| `cmd/obol/llm.go` | `obol llm configure` CLI command |
| `internal/embed/infrastructure/base/templates/llm.yaml` | llmspy Kubernetes resource definitions |
| `internal/openclaw/openclaw.go` | `Setup()`, `interactiveSetup()`, `generateOverlayValues()`, `buildLLMSpyRoutedOverlay()` |
| `internal/openclaw/import.go` | `DetectExistingConfig()`, `TranslateToOverlayYAML()` |
| `internal/openclaw/chart/values.yaml` | Default per-instance model config |
| `internal/openclaw/chart/templates/_helpers.tpl` | Renders model providers into application JSON config |

## Network Install Implementation Details

### Template Field Parser
Expand Down Expand Up @@ -786,13 +921,22 @@ obol network delete ethereum-<generated-name> --force
- `internal/network/network.go` - Network deployment
- `internal/embed/embed.go` - Embedded asset management

**LLM and OpenClaw**:
- `internal/llm/llm.go` - llmspy gateway configuration (`ConfigureLLMSpy()`)
- `cmd/obol/llm.go` - `obol llm configure` CLI command
- `internal/embed/infrastructure/base/templates/llm.yaml` - llmspy K8s resources
- `internal/openclaw/openclaw.go` - OpenClaw setup, overlay generation, llmspy routing
- `internal/openclaw/import.go` - Existing config detection and translation
- `internal/openclaw/chart/` - OpenClaw Helm chart (values, templates, helpers)

**Embedded assets**:
- `internal/embed/k3d-config.yaml` - k3d configuration template
- `internal/embed/networks/` - Network definitions
- `ethereum/helmfile.yaml.gotmpl`
- `helios/helmfile.yaml.gotmpl`
- `aztec/helmfile.yaml.gotmpl`
- `internal/embed/defaults/` - Default stack resources
- `internal/embed/infrastructure/` - Infrastructure resources (llmspy, Traefik)

**Build and version**:
- `justfile` - Task runner (install, build, up, down commands)
Expand Down Expand Up @@ -832,3 +976,12 @@ This file should be updated when:
- New workflows or development practices are established

Always confirm with the user before making updates to maintain accuracy and relevance.

## Related Codebases (External Resources)

| Resource | Path | Description |
|----------|------|-------------|
| obol-stack-front-end | `/Users/bussyjd/Development/Obol_Workbench/obol-stack-front-end` | Next.js web dashboard |
| obol-stack-docs | `/Users/bussyjd/Development/Obol_Workbench/obol-stack-docs` | MkDocs documentation site |
| OpenClaw | `/Users/bussyjd/Development/Obol_Workbench/openclaw` | OpenClaw AI assistant (upstream) |
| llmspy | `/Users/bussyjd/Development/R&D/llmspy` | LLM proxy/router (upstream) |
11 changes: 11 additions & 0 deletions Dockerfile.inference-gateway
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM golang:1.25-alpine AS builder

WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /inference-gateway ./cmd/inference-gateway

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /inference-gateway /inference-gateway
ENTRYPOINT ["/inference-gateway"]
Loading