Initial scaffold: project structure, CRDs, Makefile

- Agent CRD: deployment spec, workspace config, channels, judge integration
- AgentGroup CRD: multi-agent orchestration with shared configs
- JudgePolicy CRD: cluster-scoped authorization policies with per-agent overrides
- Makefile: build, generate, install, test, helm targets
- Go module: controller-runtime + apimachinery dependencies
This commit is contained in:
cos Agent 2026-05-11 20:54:10 -07:00
parent e9c196cd60
commit 1a831bdfe5
7 changed files with 611 additions and 1 deletions

70
Makefile Normal file
View file

@ -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}

View file

@ -1,3 +1,96 @@
# kubeclaw
OpenClaw operator for Kubernetes — CRDs, controller, and Helm chart for running Scobleclaw agents on K8s
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

191
api/v1alpha1/agent_types.go Normal file
View file

@ -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{})
}

View file

@ -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{})
}

View file

@ -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
)

View file

@ -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{})
}

14
go.mod Normal file
View file

@ -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
)