diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f7cd526 --- /dev/null +++ b/Makefile @@ -0,0 +1,70 @@ +.PHONY: all build run install uninstall generate manifests test lint + +# Container image settings +IMG ?= ghcr.io/sscoble/kubeclaw:latest +CONTAINER_RUNTIME ?= docker + +# Environment +KUBECTL ?= kubectl +CONTROLLER_GEN ?= $(shell go env GOPATH)/bin/controller-gen + +all: build + +##@ Build + +build: manifests generate fmt vet + go build -o bin/manager ./cmd/manager + +run: manifests generate fmt vet + go run ./cmd/manager + +##@ CRDs + +manifests: $(CONTROLLER_GEN) + $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./api/..." output:crd:artifacts:config=config/crd/bases + +generate: $(CONTROLLER_GEN) + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./api/..." + +install: manifests + $(KUBECTL) apply -f config/crd/bases + +uninstall: manifests + $(KUBECTL) delete -f config/crd/bases + +##@ Test + +test: manifests generate fmt vet + go test ./... -coverprofile cover.out + +##@ Lint + +fmt: + go fmt ./... + +vet: + go vet ./... + +lint: + golangci-lint run + +##@ Dependencies + +$(CONTROLLER_GEN): + go install sigs.k8s.io/controller-runtime/cmd/controller-gen@latest + +##@ Helm + +helm-package: + helm package charts/kubeclaw -d bin/ + +helm-template: + helm template kubeclaw charts/kubeclaw + +##@ Container + +docker-build: + $(CONTAINER_RUNTIME) build -t ${IMG} . + +docker-push: + $(CONTAINER_RUNTIME) push ${IMG} \ No newline at end of file diff --git a/README.md b/README.md index 4689608..5f59c42 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,96 @@ # kubeclaw -OpenClaw operator for Kubernetes — CRDs, controller, and Helm chart for running Scobleclaw agents on K8s \ No newline at end of file +OpenClaw operator for Kubernetes. + +## What it does + +Kubeclaw runs Scobleclaw agents as Kubernetes workloads. It provides: + +- **CRDs** for declaring agents, tool bindings, and judge policies as K8s resources +- **Controller** that reconciles desired state → running pods with proper config injection +- **Agent Judge integration** as a K8s admission webhook for per-tool authorization +- **Helm chart** for easy deployment + +## Architecture + +``` +┌─────────────────────────────────────────────┐ +│ Kubeclaw Controller Manager │ +│ │ +│ ┌─────────┐ ┌──────────┐ ┌───────────┐ │ +│ │ Agent │ │ Agent │ │ Judge │ │ +│ │ Reconciler│ │Group │ │ Webhook │ │ +│ │ │ │ Reconciler│ │ Handler │ │ +│ └────┬────┘ └────┬─────┘ └─────┬─────┘ │ +│ │ │ │ │ +│ ┌────▼────┐ ┌────▼─────┐ ┌────▼─────┐ │ +│ │ ConfigMap│ │ Secret │ │ Validating│ │ +│ │ Generator│ │ Generator│ │ Admission │ │ +│ └─────────┘ └──────────┘ └───────────┘ │ +└─────────────────────────────────────────────┘ +``` + +## CRDs + +### Agent +Defines a single Scobleclaw agent deployment. + +```yaml +apiVersion: scobleclaw.ai/v1alpha1 +kind: Agent +metadata: + name: marcus-chief-of-staff +spec: + model: deepseek-v4-pro:cloud + workspace: + configMapRef: marcus-workspace + channels: + - discord: + botTokenSecretRef: + name: discord-bot-token + key: token + judge: + enabled: true + model: deepseek-v4-pro:cloud + shadowMode: false + resources: + requests: + memory: "512Mi" + cpu: "250m" +``` + +### AgentGroup +Manages a group of related agents (e.g., all Scobleclaw departments). + +### JudgePolicy +Defines authorization policies for the Agent Judge admission webhook. + +## Quick Start + +```bash +# Install CRDs +make install + +# Run controller locally +make run + +# Deploy to cluster +helm install kubeclaw ./charts/kubeclaw +``` + +## Development + +```bash +# Generate CRD manifests and client code +make generate + +# Run tests +make test + +# Build manager binary +make build +``` + +## License + +MIT \ No newline at end of file diff --git a/api/v1alpha1/agent_types.go b/api/v1alpha1/agent_types.go new file mode 100644 index 0000000..9260c7c --- /dev/null +++ b/api/v1alpha1/agent_types.go @@ -0,0 +1,191 @@ +/* +Copyright 2025 Scott Scoble. + +Licensed under the MIT License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// AgentSpec defines the desired state of an Agent. +type AgentSpec struct { + // Model is the LLM model identifier (e.g., "deepseek-v4-pro:cloud"). + Model string `json:"model"` + + // Thinking is the thinking level override (e.g., "off", "low", "high"). + // +optional + Thinking string `json:"thinking,omitempty"` + + // Workspace configures the agent workspace volume and config. + Workspace AgentWorkspace `json:"workspace"` + + // Channels configures the communication channels for this agent. + // +optional + Channels []AgentChannel `json:"channels,omitempty"` + + // Judge configures the Agent Judge authorization layer. + // +optional + Judge AgentJudgeConfig `json:"judge,omitempty"` + + // Resources defines compute resource requirements. + // +optional + Resources ResourceRequirements `json:"resources,omitempty"` + + // Image is the container image to run. Defaults to the Kubeclaw agent runner. + // +optional + Image string `json:"image,omitempty"` + + // Replicas is the number of agent instances. Defaults to 1. + // +optional + Replicas *int32 `json:"replicas,omitempty"` + + // AgentID is the OpenClaw agent identifier (e.g., "main", "engineering"). + AgentID string `json:"agentId"` +} + +// AgentWorkspace configures the workspace for the agent. +type AgentWorkspace struct { + // ConfigMapRef references a ConfigMap containing agent config files. + ConfigMapRef string `json:"configMapRef"` + + // SecretRefs lists Secrets containing sensitive agent data (API keys, tokens). + // +optional + SecretRefs []string `json:"secretRefs,omitempty"` + + // PVCName references an optional persistent volume for workspace data. + // +optional + PVCName string `json:"pvcName,omitempty"` +} + +// AgentChannel configures a communication channel binding. +type AgentChannel struct { + // Type is the channel type (e.g., "discord", "slack", "telegram"). + Type string `json:"type"` + + // BotTokenSecretRef references a Secret key containing the bot token. + BotTokenSecretRef SecretKeyRef `json:"botTokenSecretRef"` + + // ChannelIDs is the list of channel IDs this agent should listen to. + // +optional + ChannelIDs []string `json:"channelIds,omitempty"` + + // Enabled controls whether this channel is active. + // +optional + Enabled bool `json:"enabled,omitempty"` +} + +// SecretKeyRef references a key in a Kubernetes Secret. +type SecretKeyRef struct { + // Name of the Secret. + Name string `json:"name"` + + // Key within the Secret. + Key string `json:"key"` +} + +// AgentJudgeConfig configures the Agent Judge authorization layer. +type AgentJudgeConfig struct { + // Enabled controls whether the judge is active. + Enabled bool `json:"enabled"` + + // Model is the judge LLM model identifier. + Model string `json:"model"` + + // ShadowMode runs the judge in shadow mode (logs but doesn't enforce). + // +optional + ShadowMode bool `json:"shadowMode,omitempty"` + + // Tiers configures tool classification tiers. + // +optional + Tiers JudgeTiers `json:"tiers,omitempty"` + + // MaxClarifyRetries is the maximum number of CLARIFY rounds before auto-reject. + // +optional + MaxClarifyRetries int `json:"maxClarifyRetries,omitempty"` + + // AuditDir is the directory for judge audit logs (inside the container). + // +optional + AuditDir string `json:"auditDir,omitempty"` +} + +// JudgeTiers configures tool classification into risk tiers. +type JudgeTiers struct { + // NoJudge tools are passed through without judge review. + // +optional + NoJudge []string `json:"noJudge,omitempty"` + + // LightweightApproval tools get pattern-checked and logged. + // +optional + LightweightApproval []string `json:"lightweightApproval,omitempty"` + + // FullJudgeReview tools require full LLM judge evaluation. + // +optional + FullJudgeReview []string `json:"fullJudgeReview,omitempty"` + + // HumanOnly tools always require human approval. + // +optional + HumanOnly []string `json:"humanOnly,omitempty"` +} + +// ResourceRequirements mirrors K8s resource requirements. +type ResourceRequirements struct { + // Requests are the minimum required resources. + // +optional + Requests map[string]string `json:"requests,omitempty"` + + // Limits are the maximum allowed resources. + // +optional + Limits map[string]string `json:"limits,omitempty"` +} + +// AgentStatus defines the observed state of an Agent. +type AgentStatus struct { + // Phase is the current phase of the agent deployment. + // +optional + Phase string `json:"phase,omitempty"` + + // ReadyReplicas is the number of ready pod replicas. + // +optional + ReadyReplicas int32 `json:"readyReplicas,omitempty"` + + // Conditions holds the conditions for this agent. + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // LastJudgeAudit is the timestamp of the last judge audit entry. + // +optional + LastJudgeAudit string `json:"lastJudgeAudit,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced +// +kubebuilder:printcolumn:name="Model",type=string,JSONPath=`.spec.model` +// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Ready",type=integer,JSONPath=`.status.readyReplicas` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` + +// Agent is the Schema for the agents API. +type Agent struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec AgentSpec `json:"spec,omitempty"` + Status AgentStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// AgentList contains a list of Agent. +type AgentList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Agent `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Agent{}, &AgentList{}) +} \ No newline at end of file diff --git a/api/v1alpha1/agentgroup_types.go b/api/v1alpha1/agentgroup_types.go new file mode 100644 index 0000000..2a4d029 --- /dev/null +++ b/api/v1alpha1/agentgroup_types.go @@ -0,0 +1,112 @@ +/* +Copyright 2025 Scott Scoble. + +Licensed under the MIT License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// AgentGroupSpec defines the desired state of an AgentGroup. +type AgentGroupSpec struct { + // Agents lists the agents in this group. + Agents []AgentGroupMember `json:"agents"` + + // SharedWorkspace references a ConfigMap with shared workspace config. + // +optional + SharedWorkspaceConfigMap string `json:"sharedWorkspaceConfigMap,omitempty"` + + // SharedSecrets references Secrets shared by all agents in the group. + // +optional + SharedSecrets []string `json:"sharedSecrets,omitempty"` + + // JudgePolicyRef references a JudgePolicy to apply to all agents in this group. + // +optional + JudgePolicyRef string `json:"judgePolicyRef,omitempty"` + + // NetworkPolicy controls inter-agent communication. + // +optional + NetworkPolicy NetworkPolicySpec `json:"networkPolicy,omitempty"` +} + +// AgentGroupMember defines a member of an AgentGroup. +type AgentGroupMember struct { + // Name is the Agent resource name. + Name string `json:"name"` + + // AgentID is the OpenClaw agent identifier. + AgentID string `json:"agentId"` + + // Model overrides the default model for this agent. + // +optional + Model string `json:"model,omitempty"` + + // Channels lists the channels this specific agent should bind to. + // +optional + Channels []AgentChannel `json:"channels,omitempty"` +} + +// NetworkPolicySpec controls network access between agents. +type NetworkPolicySpec struct { + // AllowInterAgent controls whether agents can communicate with each other. + AllowInterAgent bool `json:"allowInterAgent"` + + // AllowEgress controls outbound network access. + AllowEgress bool `json:"allowEgress"` + + // AllowedHosts restricts egress to specific hosts. + // +optional + AllowedHosts []string `json:"allowedHosts,omitempty"` +} + +// AgentGroupStatus defines the observed state of an AgentGroup. +type AgentGroupStatus struct { + // ReadyAgents is the count of agents with ready pods. + // +optional + ReadyAgents int `json:"readyAgents,omitempty"` + + // TotalAgents is the total count of agents in the group. + // +optional + TotalAgents int `json:"totalAgents,omitempty"` + + // Phase is the overall phase of the agent group. + // +optional + Phase string `json:"phase,omitempty"` + + // Conditions holds the conditions for this agent group. + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced +// +kubebuilder:printcolumn:name="Ready",type=integer,JSONPath=`.status.readyAgents` +// +kubebuilder:printcolumn:name="Total",type=integer,JSONPath=`.status.totalAgents` +// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` + +// AgentGroup is the Schema for the agentgroups API. +type AgentGroup struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec AgentGroupSpec `json:"spec,omitempty"` + Status AgentGroupStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// AgentGroupList contains a list of AgentGroup. +type AgentGroupList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []AgentGroup `json:"items"` +} + +func init() { + SchemeBuilder.Register(&AgentGroup{}, &AgentGroupList{}) +} \ No newline at end of file diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go new file mode 100644 index 0000000..127a412 --- /dev/null +++ b/api/v1alpha1/groupversion_info.go @@ -0,0 +1,25 @@ +/* +Copyright 2025 Scott Scoble. + +Licensed under the MIT License. +*/ + +// Package v1alpha1 contains API Schema definitions for the scobleclaw v1alpha1 API group. +// +groupName=scobleclaw.ai +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + GroupVersion = schema.GroupVersion{ + Group: "scobleclaw.ai", + Version: "v1alpha1", + } + + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + AddToScheme = SchemeBuilder.AddToScheme +) \ No newline at end of file diff --git a/api/v1alpha1/judgepolicy_types.go b/api/v1alpha1/judgepolicy_types.go new file mode 100644 index 0000000..b317359 --- /dev/null +++ b/api/v1alpha1/judgepolicy_types.go @@ -0,0 +1,105 @@ +/* +Copyright 2025 Scott Scoble. + +Licensed under the MIT License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// JudgePolicySpec defines the desired state of a JudgePolicy. +type JudgePolicySpec struct { + // Enabled controls whether this policy is active. + Enabled bool `json:"enabled"` + + // ShadowMode runs the judge in shadow mode (logs but doesn't enforce). + ShadowMode bool `json:"shadowMode"` + + // JudgeModel is the LLM model used for judging. + JudgeModel string `json:"judgeModel"` + + // JudgeProvider is the LLM provider (e.g., "ollama", "openai"). + JudgeProvider string `json:"judgeProvider"` + + // DefaultTiers sets the default tool classification. + DefaultTiers JudgeTiers `json:"defaultTiers"` + + // Overrides are per-agent or per-namespace tier overrides. + // +optional + Overrides []JudgePolicyOverride `json:"overrides,omitempty"` + + // HumanEscalation configures how human escalation works. + // +optional + HumanEscalation HumanEscalationConfig `json:"humanEscalation,omitempty"` +} + +// JudgePolicyOverride allows per-agent or per-namespace tier overrides. +type JudgePolicyOverride struct { + // MatchAgentID matches agents by their agentId. + // +optional + MatchAgentID string `json:"matchAgentId,omitempty"` + + // MatchNamespace matches agents in a specific namespace. + // +optional + MatchNamespace string `json:"matchNamespace,omitempty"` + + // Tiers overrides the default tier classification for matching agents. + Tiers JudgeTiers `json:"tiers"` +} + +// HumanEscalationConfig configures how human escalation works. +type HumanEscalationConfig struct { + // Channel is the notification channel (e.g., "discord", "slack"). + Channel string `json:"channel"` + + // ChannelID is the specific channel/room ID for notifications. + ChannelID string `json:"channelId"` + + // TimeoutMs is how long to wait for human response before auto-deny. + // +optional + TimeoutMs int `json:"timeoutMs,omitempty"` +} + +// JudgePolicyStatus defines the observed state of a JudgePolicy. +type JudgePolicyStatus struct { + // ActiveAgents is the count of agents currently using this policy. + // +optional + ActiveAgents int `json:"activeAgents,omitempty"` + + // TotalVerdicts is the total number of verdicts rendered under this policy. + // +optional + TotalVerdicts int64 `json:"totalVerdicts,omitempty"` + + // LastVerdictTime is the timestamp of the most recent verdict. + // +optional + LastVerdictTime string `json:"lastVerdictTime,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster + +// JudgePolicy is the Schema for the judgepolicies API. +type JudgePolicy struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec JudgePolicySpec `json:"spec,omitempty"` + Status JudgePolicyStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// JudgePolicyList contains a list of JudgePolicy. +type JudgePolicyList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []JudgePolicy `json:"items"` +} + +func init() { + SchemeBuilder.Register(&JudgePolicy{}, &JudgePolicyList{}) +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f958a72 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/sscoble/kubeclaw + +go 1.24 + +require ( + github.com/onsi/ginkgo/v2 v2.23.3 + github.com/onsi/gomega v1.37.0 + k8s.io/api v0.33.0 + k8s.io/apiextensions-apiserver v0.33.0 + k8s.io/apimachinery v0.33.0 + k8s.io/client-go v0.33.0 + sigs.k8s.io/controller-runtime v0.20.4 + sigs.k8s.io/yaml v1.4.0 +) \ No newline at end of file