Skip to content
Open
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
15 changes: 3 additions & 12 deletions cmd/obol/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ COMMANDS:
stack down Stop the Obol Stack
stack purge Delete stack config (use --force to also delete data)
Obol Agent:
agent init Initialize the Obol Agent with an API key
agent init Initialize the Obol Agent
Network Management:
network list List available networks
network install Install and deploy network to cluster
Expand Down Expand Up @@ -166,18 +166,9 @@ GLOBAL OPTIONS:
Subcommands: []*cli.Command{
{
Name: "init",
Usage: "Initialize the Obol Agent with an API key",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "agent-api-key",
Aliases: []string{"a"},
Usage: "API key for the Obol Agent",
EnvVars: []string{"AGENT_API_KEY"},
},
},
Usage: "Initialize the Obol Agent",
Action: func(c *cli.Context) error {
agentAPIKey := c.String("agent-api-key")
return agent.Init(cfg, agentAPIKey)
return agent.Init(cfg)
},
},
},
Expand Down
91 changes: 7 additions & 84 deletions internal/agent/agent.go
Original file line number Diff line number Diff line change
@@ -1,91 +1,14 @@
package agent

import (
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/ObolNetwork/obol-stack/internal/config"
"github.com/ObolNetwork/obol-stack/internal/stack"
)

const (
kubeconfigFile = "kubeconfig.yaml"
"github.com/ObolNetwork/obol-stack/internal/openclaw"
)

// Init initializes the Obol Agent with required secrets
func Init(cfg *config.Config, agentAPIKey string) error {
kubeconfigPath := filepath.Join(cfg.ConfigDir, kubeconfigFile)

// Check if kubeconfig exists (stack must be running)
if _, err := os.Stat(kubeconfigPath); os.IsNotExist(err) {
return fmt.Errorf("stack not running, use 'obol stack up' first")
}

// Get stack ID for logging
stackID := stack.GetStackID(cfg)
if stackID == "" {
return fmt.Errorf("stack ID not found, run 'obol stack init' first")
}

// If no API key provided via flag, try to read from stdin
if agentAPIKey == "" {
stat, _ := os.Stdin.Stat()
if (stat.Mode() & os.ModeCharDevice) == 0 {
// Data is being piped to stdin
data, err := io.ReadAll(os.Stdin)
if err == nil {
agentAPIKey = strings.TrimSpace(string(data))
}
}
}

// Validate Agent API key was provided
if agentAPIKey == "" {
return fmt.Errorf("agent API key required via --agent-api-key flag or AGENT_API_KEY environment variable. Navigate to https://aistudio.google.com/api-keys to create an API key for your Obol Agent")
}

fmt.Println("Initializing Obol Agent")
fmt.Printf("Stack ID: %s\n", stackID)
fmt.Println("Creating API key secret for Obol Agent")

kubectlPath := filepath.Join(cfg.BinDir, "kubectl")

// Create namespace (idempotent)
nsCmd := exec.Command(kubectlPath, "--kubeconfig", kubeconfigPath, "create", "namespace", "agent", "--dry-run=client", "-o", "yaml")
nsYAML, err := nsCmd.Output()
if err != nil {
return fmt.Errorf("failed to generate namespace manifest: %w", err)
}

applyNs := exec.Command(kubectlPath, "--kubeconfig", kubeconfigPath, "apply", "-f", "-")
applyNs.Stdin = strings.NewReader(string(nsYAML))
applyNs.Stdout = os.Stdout
applyNs.Stderr = os.Stderr
if err := applyNs.Run(); err != nil {
return fmt.Errorf("failed to create agent namespace: %w", err)
}

// Create secret (idempotent)
secretCmd := exec.Command(kubectlPath, "--kubeconfig", kubeconfigPath, "create", "secret", "generic", "obol-agent-api-key", "--from-literal=AGENT_API_KEY="+agentAPIKey, "--namespace=agent", "--dry-run=client", "-o", "yaml")
secretYAML, err := secretCmd.Output()
if err != nil {
return fmt.Errorf("failed to generate secret manifest: %w", err)
}

applySecret := exec.Command(kubectlPath, "--kubeconfig", kubeconfigPath, "apply", "-f", "-")
applySecret.Stdin = strings.NewReader(string(secretYAML))
applySecret.Stdout = os.Stdout
applySecret.Stderr = os.Stderr
if err := applySecret.Run(); err != nil {
return fmt.Errorf("failed to create Agent API key secret: %w", err)
}

fmt.Println("Agent API key secret created")
fmt.Println("Obol Agent initialized successfully")

return nil
// Init sets up an Obol Agent by running the OpenClaw onboard flow.
func Init(cfg *config.Config) error {
return openclaw.Onboard(cfg, openclaw.OnboardOptions{
Sync: true,
Interactive: true,
})
}
76 changes: 14 additions & 62 deletions internal/embed/infrastructure/base/templates/obol-agent.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,9 @@ subjects:

---
#------------------------------------------------------------------------------
# Deployment - Obol Agent Application
# The agent provides AI-powered Kubernetes and Obol cluster management via MCP
# Deployment - Obol Agent
# Lightweight sidecar that keeps the agent namespace healthy.
# The main agent logic is managed via `obol openclaw`.
#------------------------------------------------------------------------------
apiVersion: apps/v1
kind: Deployment
Expand All @@ -109,7 +110,7 @@ metadata:
labels:
app: obol-agent
spec:
replicas: 1 # Single instance deployment
replicas: 1
selector:
matchLabels:
app: obol-agent
Expand All @@ -118,68 +119,19 @@ spec:
labels:
app: obol-agent
spec:
serviceAccountName: obol-agent # Uses the ServiceAccount created above for RBAC
serviceAccountName: obol-agent
containers:
- name: obol-agent
image: us-east4-docker.pkg.dev/prj-d-playgrounds-f0cb/obol-agent/obol-agent-ag-ui:latest
imagePullPolicy: Always # Always pull latest image
ports:
- name: http
containerPort: 8000
protocol: TCP
env:
# REQUIRED: Agent API key from Kubernetes secret
# Secret created via: obol agent init --agent-api-key=<key>
- name: AGENT_API_KEY
valueFrom:
secretKeyRef:
name: obol-agent-api-key
key: AGENT_API_KEY
optional: true # Allow deployment even if secret doesn't exist

# PUBLIC_MODE controls Kubernetes MCP access
# false = Enable Kubernetes API access (uses RBAC permissions above)
# true = Disable Kubernetes API access (for public deployments)
- name: PUBLIC_MODE
value: "false"

# OKR-1: Default LLM backend via llms.py + Ollama Cloud
#
# The Obol Stack agent is provider-agnostic:
# - `llms.py` (LLMSpy) exposes an OpenAI-compatible API at /v1
# - LLMSpy forwards to Ollama (in-cluster), which can run `*:cloud` models
#
# Important: Ollama Cloud requires a one-time "connect" of the pod identity
# (public key derived from /root/.ollama/id_ed25519). We persist that key
# in the `llm/ollama-home` PVC so upgrades/restarts don't require re-connect.
- name: LLM_BACKEND
value: "llmspy"
- name: LLM_MODEL
value: "glm-4.7:cloud"
- name: OPENAI_API_BASE
value: "http://llmspy.llm.svc.cluster.local:8000/v1"
- name: OPENAI_API_KEY
value: "ollama"

# Health checks ensure the pod is ready to receive traffic
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 10
periodSeconds: 5

# Resource limits prevent the agent from consuming too many cluster resources
image: busybox:1.37
command: ["/bin/sh", "-c"]
args:
- |
echo "Use 'obol openclaw' to control your Obol Agent"
while true; do sleep 3600; done
resources:
limits: # Maximum allowed resources
cpu: 2000m # 2 CPU cores
memory: 4Gi # 4 GiB RAM
limits:
cpu: 10m
memory: 16Mi

---
#------------------------------------------------------------------------------
Expand Down
3 changes: 1 addition & 2 deletions internal/embed/infrastructure/helmfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ releases:
values:
- dataDir: /data
- network: "{{ .Values.network }}"
# obol-agent is disabled by default (image not publicly available).
# Set obolAgent.enabled=true to deploy it.
# obol-agent namespace and RBAC. Set obolAgent.enabled=true to deploy.
- obolAgent:
enabled: false

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ image:

repository: obolnetwork/obol-stack-front-end
pullPolicy: Always
tag: "v0.1.4"
tag: "sha-14f85b5"

service:
type: ClusterIP
Expand Down