From fedb55a7f35bc8335647a29f62f5e40923b9da42 Mon Sep 17 00:00:00 2001 From: bussyjd Date: Fri, 7 Nov 2025 16:40:45 +0400 Subject: [PATCH 01/10] wip: obol-agent template --- .../templates/obol-agent.yaml | 278 ++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 internal/embed/defaults/obol-stack-defaults/templates/obol-agent.yaml diff --git a/internal/embed/defaults/obol-stack-defaults/templates/obol-agent.yaml b/internal/embed/defaults/obol-stack-defaults/templates/obol-agent.yaml new file mode 100644 index 0000000..3cf3187 --- /dev/null +++ b/internal/embed/defaults/obol-stack-defaults/templates/obol-agent.yaml @@ -0,0 +1,278 @@ +--- +# Obol Agent Kubernetes Manifest +# This manifest deploys the Obol AI Agent with namespace-scoped RBAC permissions +# The agent can read cluster-wide resources (nodes, namespaces) but can only modify +# resources in specific namespaces: default, ethereum, l1, monitoring + +#------------------------------------------------------------------------------ +# ServiceAccount - Identity for the Obol Agent pod +#------------------------------------------------------------------------------ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: obol-agent + namespace: default + +--- +#------------------------------------------------------------------------------ +# ClusterRole - Read-only access to cluster-wide resources +# Allows the agent to list namespaces and nodes across the entire cluster +#------------------------------------------------------------------------------ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: obol-agent-cluster-reader +rules: + - apiGroups: [""] + resources: ["namespaces", "nodes"] + verbs: ["get", "list", "watch"] # Read-only access + +--- +#------------------------------------------------------------------------------ +# ClusterRoleBinding - Grants cluster-wide read access to the agent +#------------------------------------------------------------------------------ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: obol-agent-cluster-reader-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: obol-agent-cluster-reader +subjects: + - kind: ServiceAccount + name: obol-agent + namespace: default + +--- +#------------------------------------------------------------------------------ +# Namespace-Scoped Roles +# These roles grant create/update/patch permissions ONLY in specific namespaces +# Permissions: get, list, watch, create, update, patch (no delete) +#------------------------------------------------------------------------------ + +# Role for 'default' namespace +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: obol-agent-role + namespace: default +rules: + - apiGroups: [""] # Core API group + resources: ["pods", "services", "endpoints", "persistentvolumeclaims", "configmaps", "secrets"] + verbs: ["get", "list", "watch", "create", "update", "patch"] + - apiGroups: ["apps"] # Apps API group + resources: ["deployments", "statefulsets", "daemonsets", "replicasets"] + verbs: ["get", "list", "watch", "create", "update", "patch"] + - apiGroups: ["batch"] # Batch API group + resources: ["jobs", "cronjobs"] + verbs: ["get", "list", "watch", "create", "update", "patch"] + - apiGroups: [""] + resources: ["pods/log"] # Access to pod logs + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: obol-agent-binding + namespace: default +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: obol-agent-role +subjects: + - kind: ServiceAccount + name: obol-agent + namespace: default + +--- +# Role for 'ethereum' namespace +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: obol-agent-role + namespace: ethereum +rules: + - apiGroups: [""] + resources: ["pods", "services", "endpoints", "persistentvolumeclaims", "configmaps", "secrets"] + verbs: ["get", "list", "watch", "create", "update", "patch"] + - apiGroups: ["apps"] + resources: ["deployments", "statefulsets", "daemonsets", "replicasets"] + verbs: ["get", "list", "watch", "create", "update", "patch"] + - apiGroups: ["batch"] + resources: ["jobs", "cronjobs"] + verbs: ["get", "list", "watch", "create", "update", "patch"] + - apiGroups: [""] + resources: ["pods/log"] + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: obol-agent-binding + namespace: ethereum +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: obol-agent-role +subjects: + - kind: ServiceAccount + name: obol-agent + namespace: default + +--- +# Role for 'l1' namespace +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: obol-agent-role + namespace: l1 +rules: + - apiGroups: [""] + resources: ["pods", "services", "endpoints", "persistentvolumeclaims", "configmaps", "secrets"] + verbs: ["get", "list", "watch", "create", "update", "patch"] + - apiGroups: ["apps"] + resources: ["deployments", "statefulsets", "daemonsets", "replicasets"] + verbs: ["get", "list", "watch", "create", "update", "patch"] + - apiGroups: ["batch"] + resources: ["jobs", "cronjobs"] + verbs: ["get", "list", "watch", "create", "update", "patch"] + - apiGroups: [""] + resources: ["pods/log"] + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: obol-agent-binding + namespace: l1 +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: obol-agent-role +subjects: + - kind: ServiceAccount + name: obol-agent + namespace: default + +--- +# Role for 'monitoring' namespace +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: obol-agent-role + namespace: monitoring +rules: + - apiGroups: [""] + resources: ["pods", "services", "endpoints", "persistentvolumeclaims", "configmaps", "secrets"] + verbs: ["get", "list", "watch", "create", "update", "patch"] + - apiGroups: ["apps"] + resources: ["deployments", "statefulsets", "daemonsets", "replicasets"] + verbs: ["get", "list", "watch", "create", "update", "patch"] + - apiGroups: ["batch"] + resources: ["jobs", "cronjobs"] + verbs: ["get", "list", "watch", "create", "update", "patch"] + - apiGroups: [""] + resources: ["pods/log"] + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: obol-agent-binding + namespace: monitoring +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: obol-agent-role +subjects: + - kind: ServiceAccount + name: obol-agent + namespace: default + +--- +#------------------------------------------------------------------------------ +# Deployment - Obol Agent Application +# The agent provides AI-powered Kubernetes and Obol cluster management via MCP +#------------------------------------------------------------------------------ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: obol-agent + namespace: default + labels: + app: obol-agent +spec: + replicas: 1 # Single instance deployment + selector: + matchLabels: + app: obol-agent + template: + metadata: + labels: + app: obol-agent + spec: + serviceAccountName: obol-agent # Uses the ServiceAccount created above for RBAC + 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: Set your Google API key here or use a Secret + - name: GOOGLE_API_KEY + value: "" # TODO: Set your API key or reference a Secret + + # 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" + + # 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 + resources: + requests: # Minimum guaranteed resources + cpu: 500m # 0.5 CPU cores + memory: 1Gi # 1 GiB RAM + limits: # Maximum allowed resources + cpu: 2000m # 2 CPU cores + memory: 4Gi # 4 GiB RAM + +--- +#------------------------------------------------------------------------------ +# Service - Exposes the Obol Agent within the cluster +# Access the agent at: http://obol-agent.default.svc.cluster.local:8000 +#------------------------------------------------------------------------------ +apiVersion: v1 +kind: Service +metadata: + name: obol-agent + namespace: default + labels: + app: obol-agent +spec: + type: ClusterIP # Internal cluster access only (use Ingress for external access) + ports: + - port: 8000 # Service port + targetPort: http # Container port name + protocol: TCP + name: http + selector: + app: obol-agent # Routes traffic to pods with this label From c2f887aae8b4e7e18342acd54a9d5aa5f0515900 Mon Sep 17 00:00:00 2001 From: bussyjd Date: Fri, 7 Nov 2025 18:28:15 +0400 Subject: [PATCH 02/10] feat: add Google API key secret management for Obol Agent Add support for securely providing the GOOGLE_API_KEY to the Obol Agent via Kubernetes secrets instead of plaintext values in YAML. Changes: - Add --google-api-key flag (short: -g) to 'obol stack up' command - Accept GOOGLE_API_KEY environment variable as alternative input - Create 'agent' namespace and 'obol-agent-google-api-key' secret automatically - Update obol-agent.yaml to consume secret via secretKeyRef - Provide clear warnings when API key is not supplied The implementation uses kubectl dry-run + apply pattern for idempotent secret creation, matching the error handling style of existing code. Usage: obol stack up --google-api-key="your-key" obol stack up -g "your-key" GOOGLE_API_KEY="your-key" obol stack up --- cmd/obol/main.go | 11 ++++- .../templates/obol-agent.yaml | 29 +++++++----- internal/stack/stack.go | 44 +++++++++++++++++-- 3 files changed, 67 insertions(+), 17 deletions(-) diff --git a/cmd/obol/main.go b/cmd/obol/main.go index 39f3a00..7b8dfbf 100644 --- a/cmd/obol/main.go +++ b/cmd/obol/main.go @@ -96,8 +96,17 @@ GLOBAL OPTIONS: { Name: "up", Usage: "Start the Obol Stack", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "google-api-key", + Aliases: []string{"g"}, + Usage: "Google API key for Obol Agent (required for AI features)", + EnvVars: []string{"GOOGLE_API_KEY"}, + }, + }, Action: func(c *cli.Context) error { - if err := stack.Up(cfg); err != nil { + googleAPIKey := c.String("google-api-key") + if err := stack.Up(cfg, googleAPIKey); err != nil { stackID := stack.GetStackID(cfg) l, _ := logging.NewSlogLogger(logging.LoggerConfig{ StateDir: cfg.StateDir, diff --git a/internal/embed/defaults/obol-stack-defaults/templates/obol-agent.yaml b/internal/embed/defaults/obol-stack-defaults/templates/obol-agent.yaml index 3cf3187..a48f847 100644 --- a/internal/embed/defaults/obol-stack-defaults/templates/obol-agent.yaml +++ b/internal/embed/defaults/obol-stack-defaults/templates/obol-agent.yaml @@ -11,7 +11,7 @@ apiVersion: v1 kind: ServiceAccount metadata: name: obol-agent - namespace: default + namespace: agent --- #------------------------------------------------------------------------------ @@ -42,7 +42,7 @@ roleRef: subjects: - kind: ServiceAccount name: obol-agent - namespace: default + namespace: agent --- #------------------------------------------------------------------------------ @@ -56,7 +56,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: obol-agent-role - namespace: default + namespace: agent rules: - apiGroups: [""] # Core API group resources: ["pods", "services", "endpoints", "persistentvolumeclaims", "configmaps", "secrets"] @@ -75,7 +75,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: obol-agent-binding - namespace: default + namespace: agent roleRef: apiGroup: rbac.authorization.k8s.io kind: Role @@ -83,7 +83,7 @@ roleRef: subjects: - kind: ServiceAccount name: obol-agent - namespace: default + namespace: agent --- # Role for 'ethereum' namespace @@ -118,7 +118,7 @@ roleRef: subjects: - kind: ServiceAccount name: obol-agent - namespace: default + namespace: agent --- # Role for 'l1' namespace @@ -153,7 +153,7 @@ roleRef: subjects: - kind: ServiceAccount name: obol-agent - namespace: default + namespace: agent --- # Role for 'monitoring' namespace @@ -188,7 +188,7 @@ roleRef: subjects: - kind: ServiceAccount name: obol-agent - namespace: default + namespace: agent --- #------------------------------------------------------------------------------ @@ -199,7 +199,7 @@ apiVersion: apps/v1 kind: Deployment metadata: name: obol-agent - namespace: default + namespace: agent labels: app: obol-agent spec: @@ -222,9 +222,14 @@ spec: containerPort: 8000 protocol: TCP env: - # REQUIRED: Set your Google API key here or use a Secret + # REQUIRED: Google API key from Kubernetes secret + # Secret created via: obol stack up --google-api-key= - name: GOOGLE_API_KEY - value: "" # TODO: Set your API key or reference a Secret + valueFrom: + secretKeyRef: + name: obol-agent-google-api-key + key: GOOGLE_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) @@ -264,7 +269,7 @@ apiVersion: v1 kind: Service metadata: name: obol-agent - namespace: default + namespace: agent labels: app: obol-agent spec: diff --git a/internal/stack/stack.go b/internal/stack/stack.go index 77045c0..9a1c32b 100644 --- a/internal/stack/stack.go +++ b/internal/stack/stack.go @@ -106,7 +106,7 @@ func Init(cfg *config.Config, force bool) error { } // Up starts the k3d cluster -func Up(cfg *config.Config) error { +func Up(cfg *config.Config, googleAPIKey string) error { k3dConfigPath := filepath.Join(cfg.ConfigDir, k3dConfigFile) kubeconfigPath := filepath.Join(cfg.ConfigDir, kubeconfigFile) @@ -152,7 +152,7 @@ func Up(cfg *config.Config) error { return fmt.Errorf("failed to start existing cluster: %w", err) } - if err := syncDefaults(cfg, exec, l, kubeconfigPath); err != nil { + if err := syncDefaults(cfg, exec, l, kubeconfigPath, googleAPIKey); err != nil { return err } @@ -200,7 +200,7 @@ func Up(cfg *config.Config) error { return fmt.Errorf("failed to write kubeconfig: %w", err) } - if err := syncDefaults(cfg, exec, l, kubeconfigPath); err != nil { + if err := syncDefaults(cfg, exec, l, kubeconfigPath, googleAPIKey); err != nil { return err } @@ -366,9 +366,45 @@ func GetStackID(cfg *config.Config) string { // syncDefaults deploys the default infrastructure using helmfile // If deployment fails, the cluster is automatically stopped via Down() -func syncDefaults(cfg *config.Config, exec *executor.Executor, l *logging.Logger, kubeconfigPath string) error { +func syncDefaults(cfg *config.Config, exec *executor.Executor, l *logging.Logger, kubeconfigPath string, googleAPIKey string) error { l.Info("Deploying default infrastructure with helmfile") + // Create Google API Key secret if provided + if googleAPIKey != "" { + l.Info("Creating Google 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.CommandWithOutput(kubectlPath, "--kubeconfig", kubeconfigPath, "apply", "-f", "-") + applyNs.SetStdin(strings.NewReader(string(nsYAML))) + 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-google-api-key", "--from-literal=GOOGLE_API_KEY="+googleAPIKey, "--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.CommandWithOutput(kubectlPath, "--kubeconfig", kubeconfigPath, "apply", "-f", "-") + applySecret.SetStdin(strings.NewReader(string(secretYAML))) + if err := applySecret.Run(); err != nil { + return fmt.Errorf("failed to create Google API key secret: %w", err) + } + + l.Success("Google API key secret created") + } else { + l.Warn("No Google API key provided - Obol Agent AI features will not work") + l.Info("Provide via: obol stack up --google-api-key= or GOOGLE_API_KEY env var") + } + // Sync defaults using helmfile (handles Helm hooks properly) defaultsHelmfilePath := filepath.Join(cfg.ConfigDir, "defaults") helmfileCmd := exec.CommandWithOutput( From e5f88bc6cbee9e2c793052c3b058d55ca48e32f2 Mon Sep 17 00:00:00 2001 From: bussyjd Date: Fri, 14 Nov 2025 10:48:24 -0300 Subject: [PATCH 03/10] refactor: move Google API key init to dedicated obol agent init command Addresses review feedback to separate agent initialization from stack lifecycle. Changes: - Create new internal/agent package with Init() function for Google API key secret management - Add 'obol agent init' command with --google-api-key flag (-g) and GOOGLE_API_KEY env var support - Remove Google API key parameter from 'obol stack up' command - Remove Google API key secret creation logic from syncDefaults in internal/stack/stack.go - Update CLI help text to include new agent command section Usage: obol stack up # Start stack (no API key needed) obol agent init --google-api-key="key" # Initialize agent with API key obol agent init -g "key" # Short form GOOGLE_API_KEY="key" obol agent init # Via environment variable The agent init command requires the stack to be running and will create: - agent namespace (if it doesn't exist) - obol-agent-google-api-key secret in the agent namespace This separation allows users to manage agent secrets independently of the stack lifecycle, making it clearer when and how to provide sensitive credentials. --- cmd/obol/main.go | 49 +++++++++++++++++++----- internal/agent/agent.go | 84 +++++++++++++++++++++++++++++++++++++++++ internal/stack/stack.go | 44 ++------------------- 3 files changed, 127 insertions(+), 50 deletions(-) create mode 100644 internal/agent/agent.go diff --git a/cmd/obol/main.go b/cmd/obol/main.go index 7b8dfbf..563f968 100644 --- a/cmd/obol/main.go +++ b/cmd/obol/main.go @@ -7,6 +7,7 @@ import ( "path/filepath" "strings" + "github.com/ObolNetwork/obol-stack/internal/agent" "github.com/ObolNetwork/obol-stack/internal/app" "github.com/ObolNetwork/obol-stack/internal/config" "github.com/ObolNetwork/obol-stack/internal/executor" @@ -45,6 +46,9 @@ COMMANDS: stack down Stop the Obol Stack stack purge Delete stack config (use --force to also delete data) + Obol Agent: + agent init Initialize Obol Agent with Google API key + Kubernetes Tools (with auto-configured KUBECONFIG): kubectl Run kubectl with stack kubeconfig (passthrough) helm Run helm with stack kubeconfig (passthrough) @@ -96,17 +100,8 @@ GLOBAL OPTIONS: { Name: "up", Usage: "Start the Obol Stack", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "google-api-key", - Aliases: []string{"g"}, - Usage: "Google API key for Obol Agent (required for AI features)", - EnvVars: []string{"GOOGLE_API_KEY"}, - }, - }, Action: func(c *cli.Context) error { - googleAPIKey := c.String("google-api-key") - if err := stack.Up(cfg, googleAPIKey); err != nil { + if err := stack.Up(cfg); err != nil { stackID := stack.GetStackID(cfg) l, _ := logging.NewSlogLogger(logging.LoggerConfig{ StateDir: cfg.StateDir, @@ -160,6 +155,40 @@ GLOBAL OPTIONS: }, }, // ============================================================ + // Obol Agent Commands + // ============================================================ + { + Name: "agent", + Usage: "Manage Obol Agent", + Subcommands: []*cli.Command{ + { + Name: "init", + Usage: "Initialize Obol Agent with Google API key", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "google-api-key", + Aliases: []string{"g"}, + Usage: "Google API key for Obol Agent (required for AI features)", + EnvVars: []string{"GOOGLE_API_KEY"}, + }, + }, + Action: func(c *cli.Context) error { + googleAPIKey := c.String("google-api-key") + if err := agent.Init(cfg, googleAPIKey); err != nil { + stackID := stack.GetStackID(cfg) + l, _ := logging.NewSlogLogger(logging.LoggerConfig{ + StateDir: cfg.StateDir, + StackID: stackID, + }) + l.Error("Failed to initialize agent", "error", err.Error()) + return err + } + return nil + }, + }, + }, + }, + // ============================================================ // Kubernetes Tool Passthroughs (with auto-configured KUBECONFIG) // ============================================================ { diff --git a/internal/agent/agent.go b/internal/agent/agent.go new file mode 100644 index 0000000..e42db90 --- /dev/null +++ b/internal/agent/agent.go @@ -0,0 +1,84 @@ +package agent + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/ObolNetwork/obol-stack/internal/config" + "github.com/ObolNetwork/obol-stack/internal/executor" + "github.com/ObolNetwork/obol-stack/internal/logging" + "github.com/ObolNetwork/obol-stack/internal/stack" +) + +const ( + kubeconfigFile = "kubeconfig.yaml" +) + +// Init initializes the Obol Agent with required secrets +func Init(cfg *config.Config, googleAPIKey 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") + } + + // Create logger and executor + l, cleanup := logging.NewSlogLogger(logging.LoggerConfig{ + StateDir: cfg.StateDir, + StackID: stackID, + }) + defer cleanup() + + exec := executor.New(l.Logger) + defer exec.Close() + + // Validate Google API key was provided + if googleAPIKey == "" { + l.Error("Google API key required") + return fmt.Errorf("Google API key required via --google-api-key flag or GOOGLE_API_KEY environment variable") + } + + l.Info("Initializing Obol Agent") + l.Info("Creating Google 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.CommandWithOutput(kubectlPath, "--kubeconfig", kubeconfigPath, "apply", "-f", "-") + applyNs.SetStdin(strings.NewReader(string(nsYAML))) + 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-google-api-key", "--from-literal=GOOGLE_API_KEY="+googleAPIKey, "--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.CommandWithOutput(kubectlPath, "--kubeconfig", kubeconfigPath, "apply", "-f", "-") + applySecret.SetStdin(strings.NewReader(string(secretYAML))) + if err := applySecret.Run(); err != nil { + return fmt.Errorf("failed to create Google API key secret: %w", err) + } + + l.Success("Google API key secret created") + l.Success("Obol Agent initialized successfully") + l.Info("The Obol Agent deployment will now have access to Google API services") + + return nil +} diff --git a/internal/stack/stack.go b/internal/stack/stack.go index 9a1c32b..77045c0 100644 --- a/internal/stack/stack.go +++ b/internal/stack/stack.go @@ -106,7 +106,7 @@ func Init(cfg *config.Config, force bool) error { } // Up starts the k3d cluster -func Up(cfg *config.Config, googleAPIKey string) error { +func Up(cfg *config.Config) error { k3dConfigPath := filepath.Join(cfg.ConfigDir, k3dConfigFile) kubeconfigPath := filepath.Join(cfg.ConfigDir, kubeconfigFile) @@ -152,7 +152,7 @@ func Up(cfg *config.Config, googleAPIKey string) error { return fmt.Errorf("failed to start existing cluster: %w", err) } - if err := syncDefaults(cfg, exec, l, kubeconfigPath, googleAPIKey); err != nil { + if err := syncDefaults(cfg, exec, l, kubeconfigPath); err != nil { return err } @@ -200,7 +200,7 @@ func Up(cfg *config.Config, googleAPIKey string) error { return fmt.Errorf("failed to write kubeconfig: %w", err) } - if err := syncDefaults(cfg, exec, l, kubeconfigPath, googleAPIKey); err != nil { + if err := syncDefaults(cfg, exec, l, kubeconfigPath); err != nil { return err } @@ -366,45 +366,9 @@ func GetStackID(cfg *config.Config) string { // syncDefaults deploys the default infrastructure using helmfile // If deployment fails, the cluster is automatically stopped via Down() -func syncDefaults(cfg *config.Config, exec *executor.Executor, l *logging.Logger, kubeconfigPath string, googleAPIKey string) error { +func syncDefaults(cfg *config.Config, exec *executor.Executor, l *logging.Logger, kubeconfigPath string) error { l.Info("Deploying default infrastructure with helmfile") - // Create Google API Key secret if provided - if googleAPIKey != "" { - l.Info("Creating Google 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.CommandWithOutput(kubectlPath, "--kubeconfig", kubeconfigPath, "apply", "-f", "-") - applyNs.SetStdin(strings.NewReader(string(nsYAML))) - 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-google-api-key", "--from-literal=GOOGLE_API_KEY="+googleAPIKey, "--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.CommandWithOutput(kubectlPath, "--kubeconfig", kubeconfigPath, "apply", "-f", "-") - applySecret.SetStdin(strings.NewReader(string(secretYAML))) - if err := applySecret.Run(); err != nil { - return fmt.Errorf("failed to create Google API key secret: %w", err) - } - - l.Success("Google API key secret created") - } else { - l.Warn("No Google API key provided - Obol Agent AI features will not work") - l.Info("Provide via: obol stack up --google-api-key= or GOOGLE_API_KEY env var") - } - // Sync defaults using helmfile (handles Helm hooks properly) defaultsHelmfilePath := filepath.Join(cfg.ConfigDir, "defaults") helmfileCmd := exec.CommandWithOutput( From 52d01ea69204b8517c742e431d90392249f97e8f Mon Sep 17 00:00:00 2001 From: bussyjd Date: Mon, 8 Dec 2025 19:40:30 +0100 Subject: [PATCH 04/10] refactor: rebrand agent API key interface for flexibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove Google-specific branding from agent initialization to allow for future flexibility in API key providers. Changes include: - Rename CLI flag: --google-api-key → --agent-api-key - Update environment variable: GOOGLE_API_KEY → AGENT_API_KEY - Rename Kubernetes secret: obol-agent-google-api-key → obol-agent-api-key - Remove resource requests to lighten agent deployment for infrequent users - Remove l1 namespace RBAC (no longer exists with dynamic namespaces) - Add helpful link to Google AI Studio in error messages - Update all user-facing text to be provider-agnostic Addresses PR #84 review feedback. --- cmd/obol/main.go | 16 +++--- internal/agent/agent.go | 19 ++++--- .../templates/obol-agent.yaml | 52 +++---------------- 3 files changed, 24 insertions(+), 63 deletions(-) diff --git a/cmd/obol/main.go b/cmd/obol/main.go index 563f968..a70a41a 100644 --- a/cmd/obol/main.go +++ b/cmd/obol/main.go @@ -47,7 +47,7 @@ COMMANDS: stack purge Delete stack config (use --force to also delete data) Obol Agent: - agent init Initialize Obol Agent with Google API key + agent init Initialize the Obol Agent with an API key Kubernetes Tools (with auto-configured KUBECONFIG): kubectl Run kubectl with stack kubeconfig (passthrough) @@ -163,18 +163,18 @@ GLOBAL OPTIONS: Subcommands: []*cli.Command{ { Name: "init", - Usage: "Initialize Obol Agent with Google API key", + Usage: "Initialize the Obol Agent with an API key", Flags: []cli.Flag{ &cli.StringFlag{ - Name: "google-api-key", - Aliases: []string{"g"}, - Usage: "Google API key for Obol Agent (required for AI features)", - EnvVars: []string{"GOOGLE_API_KEY"}, + Name: "agent-api-key", + Aliases: []string{"a"}, + Usage: "API key for the Obol Agent", + EnvVars: []string{"AGENT_API_KEY"}, }, }, Action: func(c *cli.Context) error { - googleAPIKey := c.String("google-api-key") - if err := agent.Init(cfg, googleAPIKey); err != nil { + agentAPIKey := c.String("agent-api-key") + if err := agent.Init(cfg, agentAPIKey); err != nil { stackID := stack.GetStackID(cfg) l, _ := logging.NewSlogLogger(logging.LoggerConfig{ StateDir: cfg.StateDir, diff --git a/internal/agent/agent.go b/internal/agent/agent.go index e42db90..587f30c 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -17,7 +17,7 @@ const ( ) // Init initializes the Obol Agent with required secrets -func Init(cfg *config.Config, googleAPIKey string) error { +func Init(cfg *config.Config, agentAPIKey string) error { kubeconfigPath := filepath.Join(cfg.ConfigDir, kubeconfigFile) // Check if kubeconfig exists (stack must be running) @@ -41,14 +41,14 @@ func Init(cfg *config.Config, googleAPIKey string) error { exec := executor.New(l.Logger) defer exec.Close() - // Validate Google API key was provided - if googleAPIKey == "" { - l.Error("Google API key required") - return fmt.Errorf("Google API key required via --google-api-key flag or GOOGLE_API_KEY environment variable") + // Validate Agent API key was provided + if agentAPIKey == "" { + l.Error("Agent API key required") + 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") } l.Info("Initializing Obol Agent") - l.Info("Creating Google API key secret for Obol Agent") + l.Info("Creating API key secret for Obol Agent") kubectlPath := filepath.Join(cfg.BinDir, "kubectl") @@ -65,7 +65,7 @@ func Init(cfg *config.Config, googleAPIKey string) error { } // Create secret (idempotent) - secretCmd := exec.Command(kubectlPath, "--kubeconfig", kubeconfigPath, "create", "secret", "generic", "obol-agent-google-api-key", "--from-literal=GOOGLE_API_KEY="+googleAPIKey, "--namespace=agent", "--dry-run=client", "-o", "yaml") + 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) @@ -73,12 +73,11 @@ func Init(cfg *config.Config, googleAPIKey string) error { applySecret := exec.CommandWithOutput(kubectlPath, "--kubeconfig", kubeconfigPath, "apply", "-f", "-") applySecret.SetStdin(strings.NewReader(string(secretYAML))) if err := applySecret.Run(); err != nil { - return fmt.Errorf("failed to create Google API key secret: %w", err) + return fmt.Errorf("failed to create Agent API key secret: %w", err) } - l.Success("Google API key secret created") + l.Success("Agent API key secret created") l.Success("Obol Agent initialized successfully") - l.Info("The Obol Agent deployment will now have access to Google API services") return nil } diff --git a/internal/embed/defaults/obol-stack-defaults/templates/obol-agent.yaml b/internal/embed/defaults/obol-stack-defaults/templates/obol-agent.yaml index a48f847..dfb63ee 100644 --- a/internal/embed/defaults/obol-stack-defaults/templates/obol-agent.yaml +++ b/internal/embed/defaults/obol-stack-defaults/templates/obol-agent.yaml @@ -2,7 +2,7 @@ # Obol Agent Kubernetes Manifest # This manifest deploys the Obol AI Agent with namespace-scoped RBAC permissions # The agent can read cluster-wide resources (nodes, namespaces) but can only modify -# resources in specific namespaces: default, ethereum, l1, monitoring +# resources in specific namespaces: agent, ethereum, monitoring #------------------------------------------------------------------------------ # ServiceAccount - Identity for the Obol Agent pod @@ -120,41 +120,6 @@ subjects: name: obol-agent namespace: agent ---- -# Role for 'l1' namespace -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: obol-agent-role - namespace: l1 -rules: - - apiGroups: [""] - resources: ["pods", "services", "endpoints", "persistentvolumeclaims", "configmaps", "secrets"] - verbs: ["get", "list", "watch", "create", "update", "patch"] - - apiGroups: ["apps"] - resources: ["deployments", "statefulsets", "daemonsets", "replicasets"] - verbs: ["get", "list", "watch", "create", "update", "patch"] - - apiGroups: ["batch"] - resources: ["jobs", "cronjobs"] - verbs: ["get", "list", "watch", "create", "update", "patch"] - - apiGroups: [""] - resources: ["pods/log"] - verbs: ["get"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: obol-agent-binding - namespace: l1 -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: obol-agent-role -subjects: - - kind: ServiceAccount - name: obol-agent - namespace: agent - --- # Role for 'monitoring' namespace apiVersion: rbac.authorization.k8s.io/v1 @@ -222,13 +187,13 @@ spec: containerPort: 8000 protocol: TCP env: - # REQUIRED: Google API key from Kubernetes secret - # Secret created via: obol stack up --google-api-key= - - name: GOOGLE_API_KEY + # REQUIRED: Agent API key from Kubernetes secret + # Secret created via: obol agent init --agent-api-key= + - name: AGENT_API_KEY valueFrom: secretKeyRef: - name: obol-agent-google-api-key - key: GOOGLE_API_KEY + 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 @@ -253,9 +218,6 @@ spec: # Resource limits prevent the agent from consuming too many cluster resources resources: - requests: # Minimum guaranteed resources - cpu: 500m # 0.5 CPU cores - memory: 1Gi # 1 GiB RAM limits: # Maximum allowed resources cpu: 2000m # 2 CPU cores memory: 4Gi # 4 GiB RAM @@ -263,7 +225,7 @@ spec: --- #------------------------------------------------------------------------------ # Service - Exposes the Obol Agent within the cluster -# Access the agent at: http://obol-agent.default.svc.cluster.local:8000 +# Access the agent at: http://obol-agent.agent.svc.cluster.local:8000 #------------------------------------------------------------------------------ apiVersion: v1 kind: Service From 388f71ca232d4580e1f65422bf1e9f195492e692 Mon Sep 17 00:00:00 2001 From: bussyjd Date: Tue, 9 Dec 2025 14:02:51 +0100 Subject: [PATCH 05/10] feat: Replace holesky with hoodi in network configurations and docs --- CLAUDE.md | 12 ++++++------ README.md | 16 ++++++++-------- .../embed/networks/ethereum/helmfile.yaml.gotmpl | 1 - .../embed/networks/ethereum/values.yaml.gotmpl | 2 +- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d19991d..bc40752 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -151,7 +151,7 @@ obol 2. Parses each network's `helmfile.yaml.gotmpl` for environment variable annotations 3. Generates CLI flags automatically from annotations: ```yaml - # @enum mainnet,sepolia,holesky,hoodi + # @enum mainnet,sepolia,hoodi # @default mainnet # @description Blockchain network to deploy - network: {{.Network}} @@ -159,7 +159,7 @@ obol Becomes: `--network` flag with enum validation and default value **Network install flow**: -1. User runs: `obol network install ethereum --network=holesky --execution-client=geth` +1. User runs: `obol network install ethereum --network=hoodi --execution-client=geth` 2. CLI collects flag values into `overrides` map 3. Validates enum constraints 4. Calls `network.Install(cfg, "ethereum", overrides)` @@ -245,7 +245,7 @@ networks/ `values.yaml.gotmpl` contains configuration fields with annotations: ```yaml -# @enum mainnet,sepolia,holesky,hoodi +# @enum mainnet,sepolia,hoodi # @default mainnet # @description Blockchain network to deploy network: {{.Network}} @@ -322,7 +322,7 @@ obol network install ethereum --id prod --network=mainnet # Multiple deployments with different configs obol network install ethereum --id mainnet-01 -obol network install ethereum --id holesky-test --network=holesky +obol network install ethereum --id hoodi-test --network=hoodi # Both run simultaneously, isolated in separate namespaces ``` @@ -330,7 +330,7 @@ obol network install ethereum --id holesky-test --network=holesky 1. **Install** (config generation only): ``` - obol network install ethereum --network=holesky --execution-client=geth --id my-node + obol network install ethereum --network=hoodi --execution-client=geth --id my-node ↓ Check if directory exists: ~/.config/obol/networks/ethereum/my-node/ (fail unless --force) ↓ @@ -727,7 +727,7 @@ obol network list obol network install ethereum --help # Install with specific config -obol network install ethereum --network=holesky --execution-client=geth +obol network install ethereum --network=hoodi --execution-client=geth # Verify deployment obol kubectl get namespaces | grep ethereum diff --git a/README.md b/README.md index 7bc5d4a..17c9ea4 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ obol network install ethereum # This creates a deployment like: ethereum-nervous-otter # Install another network configuration -obol network install ethereum --network=holesky +obol network install ethereum --network=hoodi # This creates a separate deployment like: ethereum-happy-panda # View cluster resources (opens interactive terminal UI) @@ -120,7 +120,7 @@ obol k9s The stack will create a local Kubernetes cluster. Each network installation creates a uniquely-namespaced deployment instance, allowing you to run multiple configurations simultaneously. > [!TIP] -> Use `obol network list` to see all available networks. Customize installations with flags (e.g., `obol network install ethereum --network=holesky --execution-client=geth`) to create different deployment configurations. +> Use `obol network list` to see all available networks. Customize installations with flags (e.g., `obol network install ethereum --network=hoodi --execution-client=geth`) to create different deployment configurations. ## Managing Networks @@ -171,17 +171,17 @@ You can install the same network type multiple times with different configuratio obol network install ethereum --network=mainnet --execution-client=geth --consensus-client=prysm # Creates: ethereum-nervous-otter namespace -# Install Holesky testnet with Reth + Lighthouse -obol network install ethereum --network=holesky --execution-client=reth --consensus-client=lighthouse +# Install Hoodi testnet with Reth + Lighthouse +obol network install ethereum --network=hoodi --execution-client=reth --consensus-client=lighthouse # Creates: ethereum-laughing-elephant namespace -# Install another Holesky instance for testing -obol network install ethereum --network=holesky +# Install another Hoodi instance for testing +obol network install ethereum --network=hoodi # Creates: ethereum-happy-panda namespace ``` **Ethereum configuration options:** -- `--network`: Choose network (mainnet, sepolia, holesky, hoodi) +- `--network`: Choose network (mainnet, sepolia, hoodi) - `--execution-client`: Choose execution client (reth, geth, nethermind, besu, erigon, ethereumjs) - `--consensus-client`: Choose consensus client (lighthouse, prysm, teku, nimbus, lodestar, grandine) @@ -562,7 +562,7 @@ The stack will include [eRPC](https://erpc.cloud/), a specialized Ethereum load Network deployments will register their endpoints with ERPC, enabling seamless access to blockchain data across all deployed instances. For example: - `http://erpc.defaults.svc.cluster.local/ethereum/mainnet` → routes to mainnet deployment -- `http://erpc.defaults.svc.cluster.local/ethereum/holesky` → routes to holesky deployment +- `http://erpc.defaults.svc.cluster.local/ethereum/hoodi` → routes to hoodi deployment ### Advanced Tooling diff --git a/internal/embed/networks/ethereum/helmfile.yaml.gotmpl b/internal/embed/networks/ethereum/helmfile.yaml.gotmpl index e363c11..555e5f3 100644 --- a/internal/embed/networks/ethereum/helmfile.yaml.gotmpl +++ b/internal/embed/networks/ethereum/helmfile.yaml.gotmpl @@ -35,7 +35,6 @@ releases: addresses: mainnet: https://mainnet-checkpoint-sync.attestant.io sepolia: https://checkpoint-sync.sepolia.ethpandaops.io - holesky: https://checkpoint-sync.holesky.ethpandaops.io hoodi: https://checkpoint-sync.hoodi.ethpandaops.io # Execution client diff --git a/internal/embed/networks/ethereum/values.yaml.gotmpl b/internal/embed/networks/ethereum/values.yaml.gotmpl index c119259..874a0e2 100644 --- a/internal/embed/networks/ethereum/values.yaml.gotmpl +++ b/internal/embed/networks/ethereum/values.yaml.gotmpl @@ -1,7 +1,7 @@ # Configuration via CLI flags # Template fields populated by obol CLI during network installation -# @enum mainnet,sepolia,holesky,hoodi +# @enum mainnet,sepolia,hoodi # @default mainnet # @description Blockchain network to deploy network: {{.Network}} From 5117521c33d2856711a0bd76b56a8772c28d524a Mon Sep 17 00:00:00 2001 From: bussyjd Date: Tue, 9 Dec 2025 14:17:31 +0100 Subject: [PATCH 06/10] feat: agent RBAC improvements and stdin support - Refactored agent permissions to use dynamic RoleBindings per network namespace - Added stdin support for agent init command --- internal/agent/agent.go | 48 +++++++------ .../templates/obol-agent.yaml | 70 ------------------- .../networks/aztec/templates/agent-rbac.yaml | 17 +++++ .../ethereum/templates/agent-rbac.yaml | 17 +++++ .../networks/helios/helmfile.yaml.gotmpl | 20 ++++++ 5 files changed, 82 insertions(+), 90 deletions(-) create mode 100644 internal/embed/networks/aztec/templates/agent-rbac.yaml create mode 100644 internal/embed/networks/ethereum/templates/agent-rbac.yaml diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 587f30c..ada5b2e 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -2,13 +2,13 @@ package agent import ( "fmt" + "io" "os" + "os/exec" "path/filepath" "strings" "github.com/ObolNetwork/obol-stack/internal/config" - "github.com/ObolNetwork/obol-stack/internal/executor" - "github.com/ObolNetwork/obol-stack/internal/logging" "github.com/ObolNetwork/obol-stack/internal/stack" ) @@ -31,24 +31,26 @@ func Init(cfg *config.Config, agentAPIKey string) error { return fmt.Errorf("stack ID not found, run 'obol stack init' first") } - // Create logger and executor - l, cleanup := logging.NewSlogLogger(logging.LoggerConfig{ - StateDir: cfg.StateDir, - StackID: stackID, - }) - defer cleanup() - - exec := executor.New(l.Logger) - defer exec.Close() + // 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 == "" { - l.Error("Agent API key required") 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") } - l.Info("Initializing Obol Agent") - l.Info("Creating API key secret for 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") @@ -58,8 +60,11 @@ func Init(cfg *config.Config, agentAPIKey string) error { if err != nil { return fmt.Errorf("failed to generate namespace manifest: %w", err) } - applyNs := exec.CommandWithOutput(kubectlPath, "--kubeconfig", kubeconfigPath, "apply", "-f", "-") - applyNs.SetStdin(strings.NewReader(string(nsYAML))) + + 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) } @@ -70,14 +75,17 @@ func Init(cfg *config.Config, agentAPIKey string) error { if err != nil { return fmt.Errorf("failed to generate secret manifest: %w", err) } - applySecret := exec.CommandWithOutput(kubectlPath, "--kubeconfig", kubeconfigPath, "apply", "-f", "-") - applySecret.SetStdin(strings.NewReader(string(secretYAML))) + + 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) } - l.Success("Agent API key secret created") - l.Success("Obol Agent initialized successfully") + fmt.Println("Agent API key secret created") + fmt.Println("Obol Agent initialized successfully") return nil } diff --git a/internal/embed/defaults/obol-stack-defaults/templates/obol-agent.yaml b/internal/embed/defaults/obol-stack-defaults/templates/obol-agent.yaml index dfb63ee..3930d9d 100644 --- a/internal/embed/defaults/obol-stack-defaults/templates/obol-agent.yaml +++ b/internal/embed/defaults/obol-stack-defaults/templates/obol-agent.yaml @@ -85,76 +85,6 @@ subjects: name: obol-agent namespace: agent ---- -# Role for 'ethereum' namespace -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: obol-agent-role - namespace: ethereum -rules: - - apiGroups: [""] - resources: ["pods", "services", "endpoints", "persistentvolumeclaims", "configmaps", "secrets"] - verbs: ["get", "list", "watch", "create", "update", "patch"] - - apiGroups: ["apps"] - resources: ["deployments", "statefulsets", "daemonsets", "replicasets"] - verbs: ["get", "list", "watch", "create", "update", "patch"] - - apiGroups: ["batch"] - resources: ["jobs", "cronjobs"] - verbs: ["get", "list", "watch", "create", "update", "patch"] - - apiGroups: [""] - resources: ["pods/log"] - verbs: ["get"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: obol-agent-binding - namespace: ethereum -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: obol-agent-role -subjects: - - kind: ServiceAccount - name: obol-agent - namespace: agent - ---- -# Role for 'monitoring' namespace -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: obol-agent-role - namespace: monitoring -rules: - - apiGroups: [""] - resources: ["pods", "services", "endpoints", "persistentvolumeclaims", "configmaps", "secrets"] - verbs: ["get", "list", "watch", "create", "update", "patch"] - - apiGroups: ["apps"] - resources: ["deployments", "statefulsets", "daemonsets", "replicasets"] - verbs: ["get", "list", "watch", "create", "update", "patch"] - - apiGroups: ["batch"] - resources: ["jobs", "cronjobs"] - verbs: ["get", "list", "watch", "create", "update", "patch"] - - apiGroups: [""] - resources: ["pods/log"] - verbs: ["get"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: obol-agent-binding - namespace: monitoring -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: obol-agent-role -subjects: - - kind: ServiceAccount - name: obol-agent - namespace: agent - --- #------------------------------------------------------------------------------ # Deployment - Obol Agent Application diff --git a/internal/embed/networks/aztec/templates/agent-rbac.yaml b/internal/embed/networks/aztec/templates/agent-rbac.yaml new file mode 100644 index 0000000..5b330b0 --- /dev/null +++ b/internal/embed/networks/aztec/templates/agent-rbac.yaml @@ -0,0 +1,17 @@ +# Grant Obol Agent admin access to this namespace +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: obol-agent-access + # Namespace is set by Helm release + labels: + app.kubernetes.io/part-of: obol.stack + obol.stack/id: {{ .Values.id }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: admin +subjects: + - kind: ServiceAccount + name: obol-agent + namespace: agent \ No newline at end of file diff --git a/internal/embed/networks/ethereum/templates/agent-rbac.yaml b/internal/embed/networks/ethereum/templates/agent-rbac.yaml new file mode 100644 index 0000000..5b330b0 --- /dev/null +++ b/internal/embed/networks/ethereum/templates/agent-rbac.yaml @@ -0,0 +1,17 @@ +# Grant Obol Agent admin access to this namespace +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: obol-agent-access + # Namespace is set by Helm release + labels: + app.kubernetes.io/part-of: obol.stack + obol.stack/id: {{ .Values.id }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: admin +subjects: + - kind: ServiceAccount + name: obol-agent + namespace: agent \ No newline at end of file diff --git a/internal/embed/networks/helios/helmfile.yaml.gotmpl b/internal/embed/networks/helios/helmfile.yaml.gotmpl index a77ef0b..2be4293 100644 --- a/internal/embed/networks/helios/helmfile.yaml.gotmpl +++ b/internal/embed/networks/helios/helmfile.yaml.gotmpl @@ -66,3 +66,23 @@ releases: } } } + + # Grant Obol Agent access + - name: helios-agent-access + namespace: helios-{{ .Values.id }} + chart: bedag/raw + values: + - resources: + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + name: obol-agent-access + namespace: helios-{{ .Values.id }} + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: admin + subjects: + - kind: ServiceAccount + name: obol-agent + namespace: agent From d128198cbb41237825254382f1c98a664b027477 Mon Sep 17 00:00:00 2001 From: bussyjd Date: Tue, 9 Dec 2025 14:27:28 +0100 Subject: [PATCH 07/10] refactor: move obol-agent to infrastructure directory - Relocated obol-agent manifests from defaults to infrastructure/obol-agent - Added local Chart.yaml for obol-agent - Registered obol-agent release in infrastructure helmfile - Removed legacy defaults directory --- internal/embed/infrastructure/helmfile.yaml | 8 ++++++++ internal/embed/infrastructure/obol-agent/Chart.yaml | 6 ++++++ .../obol-agent}/templates/obol-agent.yaml | 0 3 files changed, 14 insertions(+) create mode 100644 internal/embed/infrastructure/obol-agent/Chart.yaml rename internal/embed/{defaults/obol-stack-defaults => infrastructure/obol-agent}/templates/obol-agent.yaml (100%) diff --git a/internal/embed/infrastructure/helmfile.yaml b/internal/embed/infrastructure/helmfile.yaml index 9f49d09..781952f 100644 --- a/internal/embed/infrastructure/helmfile.yaml +++ b/internal/embed/infrastructure/helmfile.yaml @@ -54,6 +54,14 @@ releases: values: - ./values/erpc.yaml.gotmpl + # Obol Agent + - name: obol-agent + namespace: agent + chart: ./obol-agent + createNamespace: true + needs: + - kube-system/base + # Obol Stack frontend - name: obol-frontend namespace: obol-frontend diff --git a/internal/embed/infrastructure/obol-agent/Chart.yaml b/internal/embed/infrastructure/obol-agent/Chart.yaml new file mode 100644 index 0000000..7972197 --- /dev/null +++ b/internal/embed/infrastructure/obol-agent/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: obol-agent +description: Obol Agent Deployment +type: application +version: 0.1.0 +appVersion: "1.0.0" diff --git a/internal/embed/defaults/obol-stack-defaults/templates/obol-agent.yaml b/internal/embed/infrastructure/obol-agent/templates/obol-agent.yaml similarity index 100% rename from internal/embed/defaults/obol-stack-defaults/templates/obol-agent.yaml rename to internal/embed/infrastructure/obol-agent/templates/obol-agent.yaml From 29f1e1539af3c9e63b61fc4157fb145b2dffb886 Mon Sep 17 00:00:00 2001 From: bussyjd Date: Tue, 9 Dec 2025 14:29:13 +0100 Subject: [PATCH 08/10] fix: update main.go agent init action This commit applies the necessary change to cmd/obol/main.go to align with the agent.Init refactor, removing direct logging and executor calls from the action function. --- cmd/obol/main.go | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/cmd/obol/main.go b/cmd/obol/main.go index afe6643..34ec34f 100644 --- a/cmd/obol/main.go +++ b/cmd/obol/main.go @@ -146,16 +146,7 @@ GLOBAL OPTIONS: }, Action: func(c *cli.Context) error { agentAPIKey := c.String("agent-api-key") - if err := agent.Init(cfg, agentAPIKey); err != nil { - stackID := stack.GetStackID(cfg) - l, _ := logging.NewSlogLogger(logging.LoggerConfig{ - StateDir: cfg.StateDir, - StackID: stackID, - }) - l.Error("Failed to initialize agent", "error", err.Error()) - return err - } - return nil + return agent.Init(cfg, agentAPIKey) }, }, }, From 762275bb17e54a4e68ccd68031573397c8721f24 Mon Sep 17 00:00:00 2001 From: bussyjd Date: Tue, 9 Dec 2025 14:43:24 +0100 Subject: [PATCH 09/10] refactor: merge obol-agent into base chart - Moved obol-agent.yaml to base/templates/ - Added Namespace definition to obol-agent.yaml - Removed separate obol-agent release from helmfile - Removed separate obol-agent chart directory --- .../templates/obol-agent.yaml | 21 ++++++++++++------- internal/embed/infrastructure/helmfile.yaml | 8 ------- .../infrastructure/obol-agent/Chart.yaml | 6 ------ 3 files changed, 14 insertions(+), 21 deletions(-) rename internal/embed/infrastructure/{obol-agent => base}/templates/obol-agent.yaml (91%) delete mode 100644 internal/embed/infrastructure/obol-agent/Chart.yaml diff --git a/internal/embed/infrastructure/obol-agent/templates/obol-agent.yaml b/internal/embed/infrastructure/base/templates/obol-agent.yaml similarity index 91% rename from internal/embed/infrastructure/obol-agent/templates/obol-agent.yaml rename to internal/embed/infrastructure/base/templates/obol-agent.yaml index 3930d9d..f73dda7 100644 --- a/internal/embed/infrastructure/obol-agent/templates/obol-agent.yaml +++ b/internal/embed/infrastructure/base/templates/obol-agent.yaml @@ -2,8 +2,17 @@ # Obol Agent Kubernetes Manifest # This manifest deploys the Obol AI Agent with namespace-scoped RBAC permissions # The agent can read cluster-wide resources (nodes, namespaces) but can only modify -# resources in specific namespaces: agent, ethereum, monitoring +# resources in specific namespaces: agent (and others via dynamic bindings) +#------------------------------------------------------------------------------ +# Namespace - Ensure the agent namespace exists +#------------------------------------------------------------------------------ +apiVersion: v1 +kind: Namespace +metadata: + name: agent + +--- #------------------------------------------------------------------------------ # ServiceAccount - Identity for the Obol Agent pod #------------------------------------------------------------------------------ @@ -46,12 +55,9 @@ subjects: --- #------------------------------------------------------------------------------ -# Namespace-Scoped Roles -# These roles grant create/update/patch permissions ONLY in specific namespaces -# Permissions: get, list, watch, create, update, patch (no delete) +# Role for 'agent' namespace +# Grants create/update/patch permissions within the agent's own namespace #------------------------------------------------------------------------------ - -# Role for 'default' namespace apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: @@ -70,6 +76,7 @@ rules: - apiGroups: [""] resources: ["pods/log"] # Access to pod logs verbs: ["get"] + --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding @@ -172,4 +179,4 @@ spec: protocol: TCP name: http selector: - app: obol-agent # Routes traffic to pods with this label + app: obol-agent # Routes traffic to pods with this label \ No newline at end of file diff --git a/internal/embed/infrastructure/helmfile.yaml b/internal/embed/infrastructure/helmfile.yaml index 781952f..9f49d09 100644 --- a/internal/embed/infrastructure/helmfile.yaml +++ b/internal/embed/infrastructure/helmfile.yaml @@ -54,14 +54,6 @@ releases: values: - ./values/erpc.yaml.gotmpl - # Obol Agent - - name: obol-agent - namespace: agent - chart: ./obol-agent - createNamespace: true - needs: - - kube-system/base - # Obol Stack frontend - name: obol-frontend namespace: obol-frontend diff --git a/internal/embed/infrastructure/obol-agent/Chart.yaml b/internal/embed/infrastructure/obol-agent/Chart.yaml deleted file mode 100644 index 7972197..0000000 --- a/internal/embed/infrastructure/obol-agent/Chart.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: v2 -name: obol-agent -description: Obol Agent Deployment -type: application -version: 0.1.0 -appVersion: "1.0.0" From 7d50e59ffcc16418b57874220d302821dc21b927 Mon Sep 17 00:00:00 2001 From: bussyjd Date: Tue, 16 Dec 2025 15:32:27 +0100 Subject: [PATCH 10/10] feat: add Renovate configuration for obol-stack-front-end version management - Add renovate.json with custom regex manager for obol-stack-front-end GitHub releases - Configure automatic PR creation for version updates - Add major version update approval requirement - Update obol-frontend.yaml.gotmpl to use semantic version tag (v0.1.1) instead of 'latest' - Enable hourly Renovate checks for frontend updates - Replace custom GitHub Actions workflow with Renovate automation --- .../values/obol-frontend.yaml.gotmpl | 2 +- renovate.json | 81 +++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 renovate.json diff --git a/internal/embed/infrastructure/values/obol-frontend.yaml.gotmpl b/internal/embed/infrastructure/values/obol-frontend.yaml.gotmpl index f7a69f1..3301156 100644 --- a/internal/embed/infrastructure/values/obol-frontend.yaml.gotmpl +++ b/internal/embed/infrastructure/values/obol-frontend.yaml.gotmpl @@ -13,7 +13,7 @@ image: repository: obolnetwork/obol-stack-front-end pullPolicy: Always - tag: "latest" + tag: "v0.1.1" service: type: ClusterIP diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..6932b83 --- /dev/null +++ b/renovate.json @@ -0,0 +1,81 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended" + ], + "enabledManagers": [ + "custom.regex", + "github-actions" + ], + "customManagers": [ + { + "customType": "regex", + "description": "Update obol-stack-front-end version from GitHub releases", + "matchStrings": [ + "tag:\\s*[\"'](?v[0-9]+\\.[0-9]+\\.[0-9]+)[\"']" + ], + "fileMatch": [ + "^internal/embed/infrastructure/values/obol-frontend\\.yaml\\.gotmpl$" + ], + "datasourceTemplate": "github-releases", + "depNameTemplate": "ObolNetwork/obol-stack-front-end", + "versioningTemplate": "semver" + } + ], + "packageRules": [ + { + "matchManagers": [ + "github-actions" + ], + "matchDepTypes": [ + "github-actions" + ], + "matchFileNames": [ + ".github/workflows/**" + ], + "schedule": [ + "every hour" + ], + "labels": [ + "renovate/github-actions" + ], + "groupName": "GitHub Actions updates" + }, + { + "description": "Group obol-stack-front-end updates", + "matchDatasources": [ + "github-releases" + ], + "matchPackageNames": [ + "ObolNetwork/obol-stack-front-end" + ], + "labels": [ + "renovate/frontend", + "obol-stack-front-end" + ], + "schedule": [ + "every hour" + ], + "groupName": "obol-stack-front-end updates", + "prBodyTemplate": "This PR updates **obol-stack-front-end** to version {{newVersion}}.\n\n### What Changed\n- **Current Version**: `{{currentVersion}}`\n- **New Version**: `{{newVersion}}`\n- **Change Type**: {{#if isMajor}}🔴 Major{{else}}{{#if isMinor}}🟡 Minor{{else}}🟢 Patch{{/if}}{{/if}}\n\n### Release Notes\n\n{{{changelog}}}\n\n### Files Updated\n{{#each upgrades}}- `{{depName}}`: `{{currentVersion}}` → `{{newVersion}}`\n{{/each}}\n\n---\n**Auto-generated by Renovate Bot**" + }, + { + "description": "Require approval for major obol-stack-front-end updates", + "matchDatasources": [ + "github-releases" + ], + "matchPackageNames": [ + "ObolNetwork/obol-stack-front-end" + ], + "matchUpdateTypes": [ + "major" + ], + "labels": [ + "renovate/major-update", + "requires-review" + ], + "dependencyDashboardApproval": true, + "prBodyTemplate": "⚠️ **MAJOR VERSION UPDATE** ⚠️\n\nThis PR updates **obol-stack-front-end** from `{{currentVersion}}` to `{{newVersion}}`.\n\n### ⚠️ Breaking Changes Expected\n\nMajor version updates may include breaking changes. Please review the release notes carefully.\n\n### Release Notes\n\n{{{changelog}}}\n\n### Migration Checklist\n- [ ] Review breaking changes in release notes\n- [ ] Test the new version in staging environment\n- [ ] Update any integration code if needed\n- [ ] Verify deployment scripts still work\n\n---\n**⚠️ This PR requires manual approval due to major version change**\n**Auto-generated by Renovate Bot**" + } + ] +}