diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..00dd246 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM golang:1.24-alpine AS builder +WORKDIR /workspace +COPY go.mod go.sum* ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -o manager ./cmd/manager + +FROM alpine:3.21 +RUN apk add --no-cache ca-certificates tzdata +WORKDIR / +COPY --from=builder /workspace/manager . +USER 65532:65532 +ENTRYPOINT ["/manager"] diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go index 127a412..e46c847 100644 --- a/api/v1alpha1/groupversion_info.go +++ b/api/v1alpha1/groupversion_info.go @@ -4,8 +4,9 @@ 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 contains API Schema definitions for the kubeclaw v1alpha1 API group. +// +groupName=kubeclaw.scoble.me +// +kubebuilder:object:generate=true package v1alpha1 import ( @@ -15,7 +16,7 @@ import ( var ( GroupVersion = schema.GroupVersion{ - Group: "scobleclaw.ai", + Group: "kubeclaw.scoble.me", Version: "v1alpha1", } diff --git a/api/v1alpha1/workflowtemplate_types.go b/api/v1alpha1/workflowtemplate_types.go index 6e7d4b0..05f1d68 100644 --- a/api/v1alpha1/workflowtemplate_types.go +++ b/api/v1alpha1/workflowtemplate_types.go @@ -8,6 +8,7 @@ package v1alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" ) // WorkflowTemplateSpec defines a workflow template that agents can instantiate. @@ -22,14 +23,14 @@ type WorkflowTemplateSpec struct { Steps []WorkflowStep `json:"steps"` // ParametersSchema defines the JSON Schema for allowed parameters. - // +kubebuilder:validation:Schemaless + // +optional // +kubebuilder:pruning:PreserveUnknownFields - ParametersSchema map[string]interface{} `json:"parametersSchema"` + ParametersSchema runtime.RawExtension `json:"parametersSchema,omitempty"` // OutputSchema defines the expected output shape (JSON Schema). - // +kubebuilder:validation:Schemaless + // +optional // +kubebuilder:pruning:PreserveUnknownFields - OutputSchema map[string]interface{} `json:"outputSchema"` + OutputSchema runtime.RawExtension `json:"outputSchema,omitempty"` // ComplianceRequirements lists compliance frameworks this template satisfies. // +optional diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000..8a708f6 --- /dev/null +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,1230 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2025 Scott Scoble. +Licensed under the MIT License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Agent) DeepCopyInto(out *Agent) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Agent. +func (in *Agent) DeepCopy() *Agent { + if in == nil { + return nil + } + out := new(Agent) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Agent) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentChannel) DeepCopyInto(out *AgentChannel) { + *out = *in + out.BotTokenSecretRef = in.BotTokenSecretRef + if in.ChannelIDs != nil { + in, out := &in.ChannelIDs, &out.ChannelIDs + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentChannel. +func (in *AgentChannel) DeepCopy() *AgentChannel { + if in == nil { + return nil + } + out := new(AgentChannel) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentConfig) DeepCopyInto(out *AgentConfig) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentConfig. +func (in *AgentConfig) DeepCopy() *AgentConfig { + if in == nil { + return nil + } + out := new(AgentConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AgentConfig) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentConfigList) DeepCopyInto(out *AgentConfigList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AgentConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentConfigList. +func (in *AgentConfigList) DeepCopy() *AgentConfigList { + if in == nil { + return nil + } + out := new(AgentConfigList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AgentConfigList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentConfigSpec) DeepCopyInto(out *AgentConfigSpec) { + *out = *in + if in.Workspace != nil { + in, out := &in.Workspace, &out.Workspace + *out = new(WorkspaceConfig) + **out = **in + } + if in.MemorySearch != nil { + in, out := &in.MemorySearch, &out.MemorySearch + *out = new(MemorySearchConfig) + **out = **in + } + if in.Heartbeat != nil { + in, out := &in.Heartbeat, &out.Heartbeat + *out = new(HeartbeatConfig) + **out = **in + } + if in.Skills != nil { + in, out := &in.Skills, &out.Skills + *out = new(SkillsConfig) + (*in).DeepCopyInto(*out) + } + if in.Sandbox != nil { + in, out := &in.Sandbox, &out.Sandbox + *out = new(SandboxConfig) + (*in).DeepCopyInto(*out) + } + if in.Tools != nil { + in, out := &in.Tools, &out.Tools + *out = new(ToolsConfig) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentConfigSpec. +func (in *AgentConfigSpec) DeepCopy() *AgentConfigSpec { + if in == nil { + return nil + } + out := new(AgentConfigSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentConfigStatus) DeepCopyInto(out *AgentConfigStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentConfigStatus. +func (in *AgentConfigStatus) DeepCopy() *AgentConfigStatus { + if in == nil { + return nil + } + out := new(AgentConfigStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentGroup) DeepCopyInto(out *AgentGroup) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentGroup. +func (in *AgentGroup) DeepCopy() *AgentGroup { + if in == nil { + return nil + } + out := new(AgentGroup) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AgentGroup) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentGroupList) DeepCopyInto(out *AgentGroupList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AgentGroup, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentGroupList. +func (in *AgentGroupList) DeepCopy() *AgentGroupList { + if in == nil { + return nil + } + out := new(AgentGroupList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AgentGroupList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentGroupMember) DeepCopyInto(out *AgentGroupMember) { + *out = *in + if in.Channels != nil { + in, out := &in.Channels, &out.Channels + *out = make([]AgentChannel, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.AllowedTemplates != nil { + in, out := &in.AllowedTemplates, &out.AllowedTemplates + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentGroupMember. +func (in *AgentGroupMember) DeepCopy() *AgentGroupMember { + if in == nil { + return nil + } + out := new(AgentGroupMember) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentGroupSpec) DeepCopyInto(out *AgentGroupSpec) { + *out = *in + if in.Agents != nil { + in, out := &in.Agents, &out.Agents + *out = make([]AgentGroupMember, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ProviderRefs != nil { + in, out := &in.ProviderRefs, &out.ProviderRefs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ChannelRefs != nil { + in, out := &in.ChannelRefs, &out.ChannelRefs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.SharedSecrets != nil { + in, out := &in.SharedSecrets, &out.SharedSecrets + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.AllowedTemplates != nil { + in, out := &in.AllowedTemplates, &out.AllowedTemplates + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.CompliancePolicyRefs != nil { + in, out := &in.CompliancePolicyRefs, &out.CompliancePolicyRefs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.NetworkPolicy != nil { + in, out := &in.NetworkPolicy, &out.NetworkPolicy + *out = new(NetworkPolicySpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentGroupSpec. +func (in *AgentGroupSpec) DeepCopy() *AgentGroupSpec { + if in == nil { + return nil + } + out := new(AgentGroupSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentGroupStatus) DeepCopyInto(out *AgentGroupStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentGroupStatus. +func (in *AgentGroupStatus) DeepCopy() *AgentGroupStatus { + if in == nil { + return nil + } + out := new(AgentGroupStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentList) DeepCopyInto(out *AgentList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Agent, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentList. +func (in *AgentList) DeepCopy() *AgentList { + if in == nil { + return nil + } + out := new(AgentList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AgentList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentSpec) DeepCopyInto(out *AgentSpec) { + *out = *in + if in.Workspace != nil { + in, out := &in.Workspace, &out.Workspace + *out = new(AgentWorkspace) + (*in).DeepCopyInto(*out) + } + if in.Providers != nil { + in, out := &in.Providers, &out.Providers + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ChannelRefs != nil { + in, out := &in.ChannelRefs, &out.ChannelRefs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Channels != nil { + in, out := &in.Channels, &out.Channels + *out = make([]AgentChannel, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.AllowedTemplates != nil { + in, out := &in.AllowedTemplates, &out.AllowedTemplates + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.AllowedDataSources != nil { + in, out := &in.AllowedDataSources, &out.AllowedDataSources + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.AllowedDestinations != nil { + in, out := &in.AllowedDestinations, &out.AllowedDestinations + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.CompliancePolicyRefs != nil { + in, out := &in.CompliancePolicyRefs, &out.CompliancePolicyRefs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(ResourceRequirements) + (*in).DeepCopyInto(*out) + } + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentSpec. +func (in *AgentSpec) DeepCopy() *AgentSpec { + if in == nil { + return nil + } + out := new(AgentSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentStatus) DeepCopyInto(out *AgentStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.LastWorkflow != nil { + in, out := &in.LastWorkflow, &out.LastWorkflow + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentStatus. +func (in *AgentStatus) DeepCopy() *AgentStatus { + if in == nil { + return nil + } + out := new(AgentStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentWorkspace) DeepCopyInto(out *AgentWorkspace) { + *out = *in + if in.SecretRefs != nil { + in, out := &in.SecretRefs, &out.SecretRefs + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentWorkspace. +func (in *AgentWorkspace) DeepCopy() *AgentWorkspace { + if in == nil { + return nil + } + out := new(AgentWorkspace) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Channel) DeepCopyInto(out *Channel) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Channel. +func (in *Channel) DeepCopy() *Channel { + if in == nil { + return nil + } + out := new(Channel) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Channel) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChannelList) DeepCopyInto(out *ChannelList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Channel, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChannelList. +func (in *ChannelList) DeepCopy() *ChannelList { + if in == nil { + return nil + } + out := new(ChannelList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ChannelList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChannelSpec) DeepCopyInto(out *ChannelSpec) { + *out = *in + if in.BotTokenSecretRef != nil { + in, out := &in.BotTokenSecretRef, &out.BotTokenSecretRef + *out = new(corev1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } + if in.AdditionalConfig != nil { + in, out := &in.AdditionalConfig, &out.AdditionalConfig + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChannelSpec. +func (in *ChannelSpec) DeepCopy() *ChannelSpec { + if in == nil { + return nil + } + out := new(ChannelSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChannelStatus) DeepCopyInto(out *ChannelStatus) { + *out = *in + if in.LastConnected != nil { + in, out := &in.LastConnected, &out.LastConnected + *out = (*in).DeepCopy() + } + if in.ConnectedAgents != nil { + in, out := &in.ConnectedAgents, &out.ConnectedAgents + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChannelStatus. +func (in *ChannelStatus) DeepCopy() *ChannelStatus { + if in == nil { + return nil + } + out := new(ChannelStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CompliancePolicy) DeepCopyInto(out *CompliancePolicy) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CompliancePolicy. +func (in *CompliancePolicy) DeepCopy() *CompliancePolicy { + if in == nil { + return nil + } + out := new(CompliancePolicy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CompliancePolicy) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CompliancePolicyList) DeepCopyInto(out *CompliancePolicyList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CompliancePolicy, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CompliancePolicyList. +func (in *CompliancePolicyList) DeepCopy() *CompliancePolicyList { + if in == nil { + return nil + } + out := new(CompliancePolicyList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CompliancePolicyList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CompliancePolicySpec) DeepCopyInto(out *CompliancePolicySpec) { + *out = *in + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]ComplianceRule, len(*in)) + copy(*out, *in) + } + out.Escalation = in.Escalation + if in.TargetSelector != nil { + in, out := &in.TargetSelector, &out.TargetSelector + *out = new(PolicyTargetSelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CompliancePolicySpec. +func (in *CompliancePolicySpec) DeepCopy() *CompliancePolicySpec { + if in == nil { + return nil + } + out := new(CompliancePolicySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CompliancePolicyStatus) DeepCopyInto(out *CompliancePolicyStatus) { + *out = *in + if in.LastJudgment != nil { + in, out := &in.LastJudgment, &out.LastJudgment + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CompliancePolicyStatus. +func (in *CompliancePolicyStatus) DeepCopy() *CompliancePolicyStatus { + if in == nil { + return nil + } + out := new(CompliancePolicyStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ComplianceRule) DeepCopyInto(out *ComplianceRule) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComplianceRule. +func (in *ComplianceRule) DeepCopy() *ComplianceRule { + if in == nil { + return nil + } + out := new(ComplianceRule) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EscalationConfig) DeepCopyInto(out *EscalationConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EscalationConfig. +func (in *EscalationConfig) DeepCopy() *EscalationConfig { + if in == nil { + return nil + } + out := new(EscalationConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HeartbeatConfig) DeepCopyInto(out *HeartbeatConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HeartbeatConfig. +func (in *HeartbeatConfig) DeepCopy() *HeartbeatConfig { + if in == nil { + return nil + } + out := new(HeartbeatConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MemorySearchConfig) DeepCopyInto(out *MemorySearchConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MemorySearchConfig. +func (in *MemorySearchConfig) DeepCopy() *MemorySearchConfig { + if in == nil { + return nil + } + out := new(MemorySearchConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ModelCost) DeepCopyInto(out *ModelCost) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ModelCost. +func (in *ModelCost) DeepCopy() *ModelCost { + if in == nil { + return nil + } + out := new(ModelCost) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkPolicySpec) DeepCopyInto(out *NetworkPolicySpec) { + *out = *in + if in.AllowedHosts != nil { + in, out := &in.AllowedHosts, &out.AllowedHosts + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkPolicySpec. +func (in *NetworkPolicySpec) DeepCopy() *NetworkPolicySpec { + if in == nil { + return nil + } + out := new(NetworkPolicySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PolicyTargetSelector) DeepCopyInto(out *PolicyTargetSelector) { + *out = *in + if in.MatchTemplates != nil { + in, out := &in.MatchTemplates, &out.MatchTemplates + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.MatchNamespaces != nil { + in, out := &in.MatchNamespaces, &out.MatchNamespaces + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.MatchLabels != nil { + in, out := &in.MatchLabels, &out.MatchLabels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PolicyTargetSelector. +func (in *PolicyTargetSelector) DeepCopy() *PolicyTargetSelector { + if in == nil { + return nil + } + out := new(PolicyTargetSelector) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Provider) DeepCopyInto(out *Provider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Provider. +func (in *Provider) DeepCopy() *Provider { + if in == nil { + return nil + } + out := new(Provider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Provider) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProviderList) DeepCopyInto(out *ProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Provider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderList. +func (in *ProviderList) DeepCopy() *ProviderList { + if in == nil { + return nil + } + out := new(ProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ProviderList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProviderModel) DeepCopyInto(out *ProviderModel) { + *out = *in + if in.Cost != nil { + in, out := &in.Cost, &out.Cost + *out = new(ModelCost) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderModel. +func (in *ProviderModel) DeepCopy() *ProviderModel { + if in == nil { + return nil + } + out := new(ProviderModel) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProviderSpec) DeepCopyInto(out *ProviderSpec) { + *out = *in + if in.APIKeySecretRef != nil { + in, out := &in.APIKeySecretRef, &out.APIKeySecretRef + *out = new(corev1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } + if in.Models != nil { + in, out := &in.Models, &out.Models + *out = make([]ProviderModel, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ExtraHeaders != nil { + in, out := &in.ExtraHeaders, &out.ExtraHeaders + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderSpec. +func (in *ProviderSpec) DeepCopy() *ProviderSpec { + if in == nil { + return nil + } + out := new(ProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProviderStatus) DeepCopyInto(out *ProviderStatus) { + *out = *in + if in.LastChecked != nil { + in, out := &in.LastChecked, &out.LastChecked + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderStatus. +func (in *ProviderStatus) DeepCopy() *ProviderStatus { + if in == nil { + return nil + } + out := new(ProviderStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceRequirements) DeepCopyInto(out *ResourceRequirements) { + *out = *in + if in.Requests != nil { + in, out := &in.Requests, &out.Requests + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Limits != nil { + in, out := &in.Limits, &out.Limits + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceRequirements. +func (in *ResourceRequirements) DeepCopy() *ResourceRequirements { + if in == nil { + return nil + } + out := new(ResourceRequirements) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SandboxConfig) DeepCopyInto(out *SandboxConfig) { + *out = *in + if in.AllowedExec != nil { + in, out := &in.AllowedExec, &out.AllowedExec + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SandboxConfig. +func (in *SandboxConfig) DeepCopy() *SandboxConfig { + if in == nil { + return nil + } + out := new(SandboxConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretKeyRef) DeepCopyInto(out *SecretKeyRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretKeyRef. +func (in *SecretKeyRef) DeepCopy() *SecretKeyRef { + if in == nil { + return nil + } + out := new(SecretKeyRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SkillsConfig) DeepCopyInto(out *SkillsConfig) { + *out = *in + if in.Allowed != nil { + in, out := &in.Allowed, &out.Allowed + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Denied != nil { + in, out := &in.Denied, &out.Denied + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SkillsConfig. +func (in *SkillsConfig) DeepCopy() *SkillsConfig { + if in == nil { + return nil + } + out := new(SkillsConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ToolsConfig) DeepCopyInto(out *ToolsConfig) { + *out = *in + if in.Allow != nil { + in, out := &in.Allow, &out.Allow + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Deny != nil { + in, out := &in.Deny, &out.Deny + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ToolsConfig. +func (in *ToolsConfig) DeepCopy() *ToolsConfig { + if in == nil { + return nil + } + out := new(ToolsConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkflowStep) DeepCopyInto(out *WorkflowStep) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkflowStep. +func (in *WorkflowStep) DeepCopy() *WorkflowStep { + if in == nil { + return nil + } + out := new(WorkflowStep) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkflowTemplate) DeepCopyInto(out *WorkflowTemplate) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkflowTemplate. +func (in *WorkflowTemplate) DeepCopy() *WorkflowTemplate { + if in == nil { + return nil + } + out := new(WorkflowTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *WorkflowTemplate) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkflowTemplateList) DeepCopyInto(out *WorkflowTemplateList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]WorkflowTemplate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkflowTemplateList. +func (in *WorkflowTemplateList) DeepCopy() *WorkflowTemplateList { + if in == nil { + return nil + } + out := new(WorkflowTemplateList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *WorkflowTemplateList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkflowTemplateSpec) DeepCopyInto(out *WorkflowTemplateSpec) { + *out = *in + if in.Steps != nil { + in, out := &in.Steps, &out.Steps + *out = make([]WorkflowStep, len(*in)) + copy(*out, *in) + } + in.ParametersSchema.DeepCopyInto(&out.ParametersSchema) + in.OutputSchema.DeepCopyInto(&out.OutputSchema) + if in.ComplianceRequirements != nil { + in, out := &in.ComplianceRequirements, &out.ComplianceRequirements + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.RequiresApproval != nil { + in, out := &in.RequiresApproval, &out.RequiresApproval + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.AllowedDataSources != nil { + in, out := &in.AllowedDataSources, &out.AllowedDataSources + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.AllowedDestinations != nil { + in, out := &in.AllowedDestinations, &out.AllowedDestinations + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkflowTemplateSpec. +func (in *WorkflowTemplateSpec) DeepCopy() *WorkflowTemplateSpec { + if in == nil { + return nil + } + out := new(WorkflowTemplateSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkflowTemplateStatus) DeepCopyInto(out *WorkflowTemplateStatus) { + *out = *in + if in.UsedByAgents != nil { + in, out := &in.UsedByAgents, &out.UsedByAgents + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.LastInstantiated != nil { + in, out := &in.LastInstantiated, &out.LastInstantiated + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkflowTemplateStatus. +func (in *WorkflowTemplateStatus) DeepCopy() *WorkflowTemplateStatus { + if in == nil { + return nil + } + out := new(WorkflowTemplateStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkspaceConfig) DeepCopyInto(out *WorkspaceConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkspaceConfig. +func (in *WorkspaceConfig) DeepCopy() *WorkspaceConfig { + if in == nil { + return nil + } + out := new(WorkspaceConfig) + in.DeepCopyInto(out) + return out +} diff --git a/bin/manager b/bin/manager new file mode 100755 index 0000000..f56339b Binary files /dev/null and b/bin/manager differ diff --git a/charts/kubeclaw/Chart.yaml b/charts/kubeclaw/Chart.yaml new file mode 100644 index 0000000..80820db --- /dev/null +++ b/charts/kubeclaw/Chart.yaml @@ -0,0 +1,18 @@ +apiVersion: v2 +name: kubeclaw +description: OpenClaw operator for Kubernetes — run Scobleclaw agents as K8s workloads +type: application +version: 0.1.0 +appVersion: "0.1.0" +keywords: + - openclaw + - agent + - llm + - ai + - operator +home: https://github.com/sscoble/kubeclaw +sources: + - https://github.com/sscoble/kubeclaw +maintainers: + - name: Scott Scoble + email: scott@scoble.me diff --git a/charts/kubeclaw/crds/kubeclaw.scoble.me_agentconfigs.yaml b/charts/kubeclaw/crds/kubeclaw.scoble.me_agentconfigs.yaml new file mode 100644 index 0000000..c7ce4e1 --- /dev/null +++ b/charts/kubeclaw/crds/kubeclaw.scoble.me_agentconfigs.yaml @@ -0,0 +1,104 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: agentconfigs.kubeclaw.scoble.me + labels: + app.kubernetes.io/name: kubeclaw + app.kubernetes.io/part-of: scobleclaw +spec: + group: kubeclaw.scoble.me + names: + kind: AgentConfig + listKind: AgentConfigList + plural: agentconfigs + singular: agentconfig + shortNames: + - acfg + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - name: Age + type: date + jsonPath: .metadata.creationTimestamp + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + workspace: + type: object + properties: + repo: + type: string + branch: + type: string + memory: + type: object + properties: + path: + type: string + autoCommit: + type: boolean + heartbeat: + type: object + properties: + intervalSeconds: + type: integer + prompt: + type: string + sandbox: + type: string + enum: [inherit, require, none] + thinking: + type: string + enum: ["off", "low", "medium", "high"] + model: + type: string + fallbacks: + type: array + items: + type: string + toolsAllow: + type: array + items: + type: string + toolsDeny: + type: array + items: + type: string + env: + type: array + items: + type: object + properties: + name: + type: string + value: + type: string + valueFrom: + type: object + properties: + secretKeyRef: + type: object + properties: + name: + type: string + key: + type: string + status: + type: object + properties: + ready: + type: boolean + usedByAgents: + type: array + items: + type: string diff --git a/charts/kubeclaw/crds/kubeclaw.scoble.me_agentgroups.yaml b/charts/kubeclaw/crds/kubeclaw.scoble.me_agentgroups.yaml new file mode 100644 index 0000000..4cc366a --- /dev/null +++ b/charts/kubeclaw/crds/kubeclaw.scoble.me_agentgroups.yaml @@ -0,0 +1,107 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: agentgroups.kubeclaw.scoble.me + labels: + app.kubernetes.io/name: kubeclaw + app.kubernetes.io/part-of: scobleclaw +spec: + group: kubeclaw.scoble.me + names: + kind: AgentGroup + listKind: AgentGroupList + plural: agentgroups + singular: agentgroup + shortNames: + - agg + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - name: Members + type: integer + jsonPath: .status.memberCount + - name: Ready + type: boolean + jsonPath: .status.ready + - name: Age + type: date + jsonPath: .metadata.creationTimestamp + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + required: [members] + properties: + sharedConfigRef: + type: string + sharedProviders: + type: array + items: + type: string + sharedChannels: + type: array + items: + type: string + members: + type: array + items: + type: object + required: [name, agentId] + properties: + name: + type: string + agentId: + type: string + model: + type: string + thinking: + type: string + configOverrides: + type: object + properties: + model: + type: string + thinking: + type: string + allowedTemplates: + type: array + items: + type: string + resources: + type: object + properties: + requests: + type: object + properties: + cpu: + type: string + memory: + type: string + limits: + type: object + properties: + cpu: + type: string + memory: + type: string + status: + type: object + properties: + ready: + type: boolean + memberCount: + type: integer + memberReady: + type: integer + agentRefs: + type: array + items: + type: string diff --git a/charts/kubeclaw/crds/kubeclaw.scoble.me_agents.yaml b/charts/kubeclaw/crds/kubeclaw.scoble.me_agents.yaml new file mode 100644 index 0000000..0c7c708 --- /dev/null +++ b/charts/kubeclaw/crds/kubeclaw.scoble.me_agents.yaml @@ -0,0 +1,147 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: agents.kubeclaw.scoble.me + labels: + app.kubernetes.io/name: kubeclaw + app.kubernetes.io/part-of: scobleclaw +spec: + group: kubeclaw.scoble.me + names: + kind: Agent + listKind: AgentList + plural: agents + singular: agent + shortNames: + - ag + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - name: AgentID + type: string + jsonPath: .spec.agentId + - name: Model + type: string + jsonPath: .spec.model + - name: Ready + type: boolean + jsonPath: .status.ready + - name: Age + type: date + jsonPath: .metadata.creationTimestamp + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + required: [agentId] + properties: + agentId: + type: string + description: OpenClaw agent identifier (main, engineering, etc.) + model: + type: string + thinking: + type: string + agentConfigRef: + type: string + description: Reference to an AgentConfig resource + workspace: + type: object + properties: + configMapRef: + type: string + secretRefs: + type: array + items: + type: string + pvcName: + type: string + providers: + type: array + items: + type: string + channelRefs: + type: array + items: + type: string + channels: + type: array + items: + type: object + required: [type, botTokenSecretRef] + properties: + type: + type: string + botTokenSecretRef: + type: object + required: [name, key] + properties: + name: + type: string + key: + type: string + guildID: + type: string + defaultChannel: + type: string + defaultTemplate: + type: string + allowedTemplates: + type: array + items: + type: string + allowedDataSources: + type: array + items: + type: string + allowedDestinations: + type: array + items: + type: string + compliancePolicyRefs: + type: array + items: + type: string + resources: + type: object + properties: + requests: + type: object + properties: + cpu: + type: string + memory: + type: string + limits: + type: object + properties: + cpu: + type: string + memory: + type: string + image: + type: string + replicas: + type: integer + status: + type: object + properties: + ready: + type: boolean + phase: + type: string + deployments: + type: array + items: + type: string + lastTransition: + type: string + format: date-time diff --git a/charts/kubeclaw/crds/kubeclaw.scoble.me_channels.yaml b/charts/kubeclaw/crds/kubeclaw.scoble.me_channels.yaml new file mode 100644 index 0000000..8cb4f3c --- /dev/null +++ b/charts/kubeclaw/crds/kubeclaw.scoble.me_channels.yaml @@ -0,0 +1,75 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: channels.kubeclaw.scoble.me + labels: + app.kubernetes.io/name: kubeclaw + app.kubernetes.io/part-of: scobleclaw +spec: + group: kubeclaw.scoble.me + names: + kind: Channel + listKind: ChannelList + plural: channels + singular: channel + shortNames: + - ch + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - name: Type + type: string + jsonPath: .spec.type + - name: Ready + type: boolean + jsonPath: .status.ready + - name: Age + type: date + jsonPath: .metadata.creationTimestamp + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + required: [type, botTokenSecretRef] + properties: + type: + type: string + description: Channel type (discord, slack, telegram) + botTokenSecretRef: + type: object + required: [name, key] + properties: + name: + type: string + key: + type: string + webhookURL: + type: string + guildID: + type: string + defaultChannel: + type: string + allowedChannels: + type: array + items: + type: string + status: + type: object + properties: + ready: + type: boolean + connected: + type: boolean + condition: + type: string + lastConnected: + type: string + format: date-time diff --git a/charts/kubeclaw/crds/kubeclaw.scoble.me_compliancepolicies.yaml b/charts/kubeclaw/crds/kubeclaw.scoble.me_compliancepolicies.yaml new file mode 100644 index 0000000..370b482 --- /dev/null +++ b/charts/kubeclaw/crds/kubeclaw.scoble.me_compliancepolicies.yaml @@ -0,0 +1,106 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: compliancepolicies.kubeclaw.scoble.me + labels: + app.kubernetes.io/name: kubeclaw + app.kubernetes.io/part-of: scobleclaw +spec: + group: kubeclaw.scoble.me + names: + kind: CompliancePolicy + listKind: CompliancePolicyList + plural: compliancepolicies + singular: compliancepolicy + shortNames: + - cp + - compol + scope: Cluster + versions: + - name: v1alpha1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - name: Framework + type: string + jsonPath: .spec.framework + - name: Escalation + type: string + jsonPath: .spec.escalation.mode + - name: Ready + type: boolean + jsonPath: .status.ready + - name: Age + type: date + jsonPath: .metadata.creationTimestamp + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + required: [displayName, framework, rules, escalation] + properties: + displayName: + type: string + framework: + type: string + rules: + type: array + items: + type: object + required: [name, description, severity, constraint] + properties: + name: + type: string + description: + type: string + severity: + type: string + enum: [block, warn, log] + constraint: + type: string + escalation: + type: object + required: [mode] + properties: + mode: + type: string + enum: [auto_deny, human_review, warn_only] + humanChannelRef: + type: string + timeoutSeconds: + type: integer + targetSelector: + type: object + properties: + matchTemplates: + type: array + items: + type: string + matchNamespaces: + type: array + items: + type: string + matchLabels: + type: object + additionalProperties: + type: string + prompt: + type: string + status: + type: object + properties: + ready: + type: boolean + activeTemplates: + type: integer + totalJudgments: + type: integer + format: int64 + lastJudgment: + type: string + format: date-time diff --git a/charts/kubeclaw/crds/kubeclaw.scoble.me_providers.yaml b/charts/kubeclaw/crds/kubeclaw.scoble.me_providers.yaml new file mode 100644 index 0000000..ccfdc34 --- /dev/null +++ b/charts/kubeclaw/crds/kubeclaw.scoble.me_providers.yaml @@ -0,0 +1,86 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: providers.kubeclaw.scoble.me + labels: + app.kubernetes.io/name: kubeclaw + app.kubernetes.io/part-of: scobleclaw +spec: + group: kubeclaw.scoble.me + names: + kind: Provider + listKind: ProviderList + plural: providers + singular: provider + shortNames: + - prov + scope: Cluster + versions: + - name: v1alpha1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - name: Type + type: string + jsonPath: .spec.type + - name: Ready + type: boolean + jsonPath: .status.ready + - name: Age + type: date + jsonPath: .metadata.creationTimestamp + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + required: [type] + properties: + type: + type: string + description: Provider type (ollama, openai, anthropic, deepseek) + baseUrl: + type: string + description: Provider API endpoint URL + apiKeySecretRef: + type: object + properties: + name: + type: string + key: + type: string + models: + type: array + items: + type: object + required: [id] + properties: + id: + type: string + default: + type: boolean + cost: + type: object + properties: + input: + type: number + output: + type: number + extraHeaders: + type: object + additionalProperties: + type: string + status: + type: object + properties: + ready: + type: boolean + condition: + type: string + lastChecked: + type: string + format: date-time diff --git a/charts/kubeclaw/crds/kubeclaw.scoble.me_workflowtemplates.yaml b/charts/kubeclaw/crds/kubeclaw.scoble.me_workflowtemplates.yaml new file mode 100644 index 0000000..0a1ecb8 --- /dev/null +++ b/charts/kubeclaw/crds/kubeclaw.scoble.me_workflowtemplates.yaml @@ -0,0 +1,106 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: workflowtemplates.kubeclaw.scoble.me + labels: + app.kubernetes.io/name: kubeclaw + app.kubernetes.io/part-of: scobleclaw +spec: + group: kubeclaw.scoble.me + names: + kind: WorkflowTemplate + listKind: WorkflowTemplateList + plural: workflowtemplates + singular: workflowtemplate + shortNames: + - wt + - wft + scope: Cluster + versions: + - name: v1alpha1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - name: Audit + type: string + jsonPath: .spec.auditLevel + - name: Steps + type: integer + jsonPath: .spec.steps + - name: Ready + type: boolean + jsonPath: .status.ready + - name: Age + type: date + jsonPath: .metadata.creationTimestamp + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + required: [displayName, description, steps, parametersSchema, outputSchema, auditLevel] + properties: + displayName: + type: string + description: + type: string + steps: + type: array + items: + type: object + required: [name, action, target] + properties: + name: + type: string + action: + type: string + target: + type: string + condition: + type: string + onFailure: + type: string + enum: [stop, skip, continue] + parametersSchema: + type: object + x-kubernetes-preserve-unknown-fields: true + outputSchema: + type: object + x-kubernetes-preserve-unknown-fields: true + complianceRequirements: + type: array + items: + type: string + requiresApproval: + type: array + items: + type: string + allowedDataSources: + type: array + items: + type: string + allowedDestinations: + type: array + items: + type: string + auditLevel: + type: string + enum: [minimal, standard, full] + status: + type: object + properties: + ready: + type: boolean + validated: + type: boolean + usedByAgents: + type: array + items: + type: string + lastInstantiated: + type: string + format: date-time diff --git a/charts/kubeclaw/templates/_helpers.tpl b/charts/kubeclaw/templates/_helpers.tpl new file mode 100644 index 0000000..eafea18 --- /dev/null +++ b/charts/kubeclaw/templates/_helpers.tpl @@ -0,0 +1,39 @@ +{{- define "kubeclaw.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "kubeclaw.fullname" -}} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} + +{{- define "kubeclaw.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "kubeclaw.labels" -}} +helm.sh/chart: {{ include "kubeclaw.chart" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{ include "kubeclaw.selectorLabels" . }} +{{- end }} + +{{- define "kubeclaw.selectorLabels" -}} +app.kubernetes.io/name: {{ include "kubeclaw.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/part-of: scobleclaw +{{- end }} + +{{- define "kubeclaw.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "kubeclaw.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/charts/kubeclaw/templates/deployment.yaml b/charts/kubeclaw/templates/deployment.yaml new file mode 100644 index 0000000..2a20817 --- /dev/null +++ b/charts/kubeclaw/templates/deployment.yaml @@ -0,0 +1,35 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "kubeclaw.fullname" . }} + labels: {{- include "kubeclaw.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: {{- include "kubeclaw.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: {{- include "kubeclaw.labels" . | nindent 8 }} + spec: + serviceAccountName: {{ include "kubeclaw.serviceAccountName" . }} + containers: + - name: manager + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + args: + - --health-probe-bind-address=:8081 + - --metrics-bind-address=:8080 + ports: + - name: metrics + containerPort: 8080 + - name: health + containerPort: 8081 + livenessProbe: + httpGet: + path: /healthz + port: health + readinessProbe: + httpGet: + path: /readyz + port: health + resources: {{- toYaml .Values.resources | nindent 12 }} diff --git a/charts/kubeclaw/templates/rbac.yaml b/charts/kubeclaw/templates/rbac.yaml new file mode 100644 index 0000000..a711aa5 --- /dev/null +++ b/charts/kubeclaw/templates/rbac.yaml @@ -0,0 +1,40 @@ +{{- if .Values.rbac.create }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "kubeclaw.fullname" . }} + labels: {{- include "kubeclaw.labels" . | nindent 4 }} +rules: + - apiGroups: ["kubeclaw.scoble.me"] + resources: ["agents", "agentconfigs", "agentgroups", "channels", "providers"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + - apiGroups: ["kubeclaw.scoble.me"] + resources: ["workflowtemplates", "compliancepolicies"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + - apiGroups: ["kubeclaw.scoble.me"] + resources: ["agents/status", "agentconfigs/status", "agentgroups/status", "channels/status", "providers/status", "workflowtemplates/status", "compliancepolicies/status"] + verbs: ["get", "update", "patch"] + - apiGroups: ["kubeclaw.scoble.me"] + resources: ["agents/finalizers"] + verbs: ["update"] + - apiGroups: [""] + resources: ["pods", "configmaps", "serviceaccounts"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "kubeclaw.fullname" . }} + labels: {{- include "kubeclaw.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "kubeclaw.fullname" . }} +subjects: + - kind: ServiceAccount + name: {{ include "kubeclaw.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/charts/kubeclaw/templates/serviceaccount.yaml b/charts/kubeclaw/templates/serviceaccount.yaml new file mode 100644 index 0000000..63c0d10 --- /dev/null +++ b/charts/kubeclaw/templates/serviceaccount.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "kubeclaw.serviceAccountName" . }} + labels: {{- include "kubeclaw.labels" . | nindent 4 }} diff --git a/charts/kubeclaw/values.yaml b/charts/kubeclaw/values.yaml new file mode 100644 index 0000000..043e33f --- /dev/null +++ b/charts/kubeclaw/values.yaml @@ -0,0 +1,31 @@ +# kubeclaw Helm chart values + +replicaCount: 1 + +image: + repository: ghcr.io/sscoble/kubeclaw + pullPolicy: IfNotPresent + tag: latest + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + create: true + name: kubeclaw-controller + +rbac: + create: true + +resources: + limits: + cpu: 500m + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi + +nodeSelector: {} +tolerations: [] +affinity: {} diff --git a/cmd/manager/main.go b/cmd/manager/main.go new file mode 100644 index 0000000..94843de --- /dev/null +++ b/cmd/manager/main.go @@ -0,0 +1,101 @@ +/* +Copyright 2025 Scott Scoble. +Licensed under the MIT License. +*/ + +package main + +import ( + "flag" + "os" + + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + kubeclawv1alpha1 "github.com/sscoble/kubeclaw/api/v1alpha1" + "github.com/sscoble/kubeclaw/internal/controller" +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(kubeclawv1alpha1.AddToScheme(scheme)) +} + +func main() { + var metricsAddr string + var enableLeaderElection bool + var probeAddr string + + flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") + flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + flag.BoolVar(&enableLeaderElection, "leader-elect", false, + "Enable leader election for controller manager.") + opts := zap.Options{Development: true} + opts.BindFlags(flag.CommandLine) + flag.Parse() + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + Metrics: ctrl.Options{}.Metrics, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "kubeclaw.scoble.me", + }) + if err != nil { + setupLog.Error(err, "unable to start manager") + os.Exit(1) + } + + // Set up Agent reconciler + if err = (&controller.AgentReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Agent") + os.Exit(1) + } + + // Set up WorkflowTemplate reconciler + if err = (&controller.WorkflowTemplateReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "WorkflowTemplate") + os.Exit(1) + } + + // Set up CompliancePolicy reconciler + if err = (&controller.CompliancePolicyReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "CompliancePolicy") + os.Exit(1) + } + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up health check") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up ready check") + os.Exit(1) + } + + setupLog.Info("starting manager") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } +} diff --git a/config/crd/bases/kubeclaw.scoble.me_agentconfigs.yaml b/config/crd/bases/kubeclaw.scoble.me_agentconfigs.yaml new file mode 100644 index 0000000..173cd3c --- /dev/null +++ b/config/crd/bases/kubeclaw.scoble.me_agentconfigs.yaml @@ -0,0 +1,110 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: agentconfigs.kubeclaw.scoble.me + labels: + app.kubernetes.io/name: kubeclaw + app.kubernetes.io/part-of: scobleclaw +spec: + group: kubeclaw.scoble.me + names: + kind: AgentConfig + listKind: AgentConfigList + plural: agentconfigs + singular: agentconfig + shortNames: + - acfg + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - name: Age + type: date + jsonPath: .metadata.creationTimestamp + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + workspace: + type: object + properties: + repo: + type: string + branch: + type: string + memory: + type: object + properties: + path: + type: string + autoCommit: + type: boolean + heartbeat: + type: object + properties: + intervalSeconds: + type: integer + prompt: + type: string + sandbox: + type: object + properties: + enabled: + type: boolean + allowedExec: + type: array + items: + type: string + thinking: + type: string + enum: ["off", "low", "medium", "high"] + model: + type: string + fallbacks: + type: array + items: + type: string + toolsAllow: + type: array + items: + type: string + toolsDeny: + type: array + items: + type: string + env: + type: array + items: + type: object + properties: + name: + type: string + value: + type: string + valueFrom: + type: object + properties: + secretKeyRef: + type: object + properties: + name: + type: string + key: + type: string + status: + type: object + properties: + ready: + type: boolean + usedByAgents: + type: array + items: + type: string diff --git a/config/crd/bases/kubeclaw.scoble.me_agentgroups.yaml b/config/crd/bases/kubeclaw.scoble.me_agentgroups.yaml new file mode 100644 index 0000000..4cc366a --- /dev/null +++ b/config/crd/bases/kubeclaw.scoble.me_agentgroups.yaml @@ -0,0 +1,107 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: agentgroups.kubeclaw.scoble.me + labels: + app.kubernetes.io/name: kubeclaw + app.kubernetes.io/part-of: scobleclaw +spec: + group: kubeclaw.scoble.me + names: + kind: AgentGroup + listKind: AgentGroupList + plural: agentgroups + singular: agentgroup + shortNames: + - agg + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - name: Members + type: integer + jsonPath: .status.memberCount + - name: Ready + type: boolean + jsonPath: .status.ready + - name: Age + type: date + jsonPath: .metadata.creationTimestamp + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + required: [members] + properties: + sharedConfigRef: + type: string + sharedProviders: + type: array + items: + type: string + sharedChannels: + type: array + items: + type: string + members: + type: array + items: + type: object + required: [name, agentId] + properties: + name: + type: string + agentId: + type: string + model: + type: string + thinking: + type: string + configOverrides: + type: object + properties: + model: + type: string + thinking: + type: string + allowedTemplates: + type: array + items: + type: string + resources: + type: object + properties: + requests: + type: object + properties: + cpu: + type: string + memory: + type: string + limits: + type: object + properties: + cpu: + type: string + memory: + type: string + status: + type: object + properties: + ready: + type: boolean + memberCount: + type: integer + memberReady: + type: integer + agentRefs: + type: array + items: + type: string diff --git a/config/crd/bases/kubeclaw.scoble.me_agents.yaml b/config/crd/bases/kubeclaw.scoble.me_agents.yaml new file mode 100644 index 0000000..0c7c708 --- /dev/null +++ b/config/crd/bases/kubeclaw.scoble.me_agents.yaml @@ -0,0 +1,147 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: agents.kubeclaw.scoble.me + labels: + app.kubernetes.io/name: kubeclaw + app.kubernetes.io/part-of: scobleclaw +spec: + group: kubeclaw.scoble.me + names: + kind: Agent + listKind: AgentList + plural: agents + singular: agent + shortNames: + - ag + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - name: AgentID + type: string + jsonPath: .spec.agentId + - name: Model + type: string + jsonPath: .spec.model + - name: Ready + type: boolean + jsonPath: .status.ready + - name: Age + type: date + jsonPath: .metadata.creationTimestamp + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + required: [agentId] + properties: + agentId: + type: string + description: OpenClaw agent identifier (main, engineering, etc.) + model: + type: string + thinking: + type: string + agentConfigRef: + type: string + description: Reference to an AgentConfig resource + workspace: + type: object + properties: + configMapRef: + type: string + secretRefs: + type: array + items: + type: string + pvcName: + type: string + providers: + type: array + items: + type: string + channelRefs: + type: array + items: + type: string + channels: + type: array + items: + type: object + required: [type, botTokenSecretRef] + properties: + type: + type: string + botTokenSecretRef: + type: object + required: [name, key] + properties: + name: + type: string + key: + type: string + guildID: + type: string + defaultChannel: + type: string + defaultTemplate: + type: string + allowedTemplates: + type: array + items: + type: string + allowedDataSources: + type: array + items: + type: string + allowedDestinations: + type: array + items: + type: string + compliancePolicyRefs: + type: array + items: + type: string + resources: + type: object + properties: + requests: + type: object + properties: + cpu: + type: string + memory: + type: string + limits: + type: object + properties: + cpu: + type: string + memory: + type: string + image: + type: string + replicas: + type: integer + status: + type: object + properties: + ready: + type: boolean + phase: + type: string + deployments: + type: array + items: + type: string + lastTransition: + type: string + format: date-time diff --git a/config/crd/bases/kubeclaw.scoble.me_channels.yaml b/config/crd/bases/kubeclaw.scoble.me_channels.yaml new file mode 100644 index 0000000..8cb4f3c --- /dev/null +++ b/config/crd/bases/kubeclaw.scoble.me_channels.yaml @@ -0,0 +1,75 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: channels.kubeclaw.scoble.me + labels: + app.kubernetes.io/name: kubeclaw + app.kubernetes.io/part-of: scobleclaw +spec: + group: kubeclaw.scoble.me + names: + kind: Channel + listKind: ChannelList + plural: channels + singular: channel + shortNames: + - ch + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - name: Type + type: string + jsonPath: .spec.type + - name: Ready + type: boolean + jsonPath: .status.ready + - name: Age + type: date + jsonPath: .metadata.creationTimestamp + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + required: [type, botTokenSecretRef] + properties: + type: + type: string + description: Channel type (discord, slack, telegram) + botTokenSecretRef: + type: object + required: [name, key] + properties: + name: + type: string + key: + type: string + webhookURL: + type: string + guildID: + type: string + defaultChannel: + type: string + allowedChannels: + type: array + items: + type: string + status: + type: object + properties: + ready: + type: boolean + connected: + type: boolean + condition: + type: string + lastConnected: + type: string + format: date-time diff --git a/config/crd/bases/kubeclaw.scoble.me_compliancepolicies.yaml b/config/crd/bases/kubeclaw.scoble.me_compliancepolicies.yaml new file mode 100644 index 0000000..370b482 --- /dev/null +++ b/config/crd/bases/kubeclaw.scoble.me_compliancepolicies.yaml @@ -0,0 +1,106 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: compliancepolicies.kubeclaw.scoble.me + labels: + app.kubernetes.io/name: kubeclaw + app.kubernetes.io/part-of: scobleclaw +spec: + group: kubeclaw.scoble.me + names: + kind: CompliancePolicy + listKind: CompliancePolicyList + plural: compliancepolicies + singular: compliancepolicy + shortNames: + - cp + - compol + scope: Cluster + versions: + - name: v1alpha1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - name: Framework + type: string + jsonPath: .spec.framework + - name: Escalation + type: string + jsonPath: .spec.escalation.mode + - name: Ready + type: boolean + jsonPath: .status.ready + - name: Age + type: date + jsonPath: .metadata.creationTimestamp + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + required: [displayName, framework, rules, escalation] + properties: + displayName: + type: string + framework: + type: string + rules: + type: array + items: + type: object + required: [name, description, severity, constraint] + properties: + name: + type: string + description: + type: string + severity: + type: string + enum: [block, warn, log] + constraint: + type: string + escalation: + type: object + required: [mode] + properties: + mode: + type: string + enum: [auto_deny, human_review, warn_only] + humanChannelRef: + type: string + timeoutSeconds: + type: integer + targetSelector: + type: object + properties: + matchTemplates: + type: array + items: + type: string + matchNamespaces: + type: array + items: + type: string + matchLabels: + type: object + additionalProperties: + type: string + prompt: + type: string + status: + type: object + properties: + ready: + type: boolean + activeTemplates: + type: integer + totalJudgments: + type: integer + format: int64 + lastJudgment: + type: string + format: date-time diff --git a/config/crd/bases/kubeclaw.scoble.me_providers.yaml b/config/crd/bases/kubeclaw.scoble.me_providers.yaml new file mode 100644 index 0000000..ccfdc34 --- /dev/null +++ b/config/crd/bases/kubeclaw.scoble.me_providers.yaml @@ -0,0 +1,86 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: providers.kubeclaw.scoble.me + labels: + app.kubernetes.io/name: kubeclaw + app.kubernetes.io/part-of: scobleclaw +spec: + group: kubeclaw.scoble.me + names: + kind: Provider + listKind: ProviderList + plural: providers + singular: provider + shortNames: + - prov + scope: Cluster + versions: + - name: v1alpha1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - name: Type + type: string + jsonPath: .spec.type + - name: Ready + type: boolean + jsonPath: .status.ready + - name: Age + type: date + jsonPath: .metadata.creationTimestamp + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + required: [type] + properties: + type: + type: string + description: Provider type (ollama, openai, anthropic, deepseek) + baseUrl: + type: string + description: Provider API endpoint URL + apiKeySecretRef: + type: object + properties: + name: + type: string + key: + type: string + models: + type: array + items: + type: object + required: [id] + properties: + id: + type: string + default: + type: boolean + cost: + type: object + properties: + input: + type: number + output: + type: number + extraHeaders: + type: object + additionalProperties: + type: string + status: + type: object + properties: + ready: + type: boolean + condition: + type: string + lastChecked: + type: string + format: date-time diff --git a/config/crd/bases/kubeclaw.scoble.me_workflowtemplates.yaml b/config/crd/bases/kubeclaw.scoble.me_workflowtemplates.yaml new file mode 100644 index 0000000..0a1ecb8 --- /dev/null +++ b/config/crd/bases/kubeclaw.scoble.me_workflowtemplates.yaml @@ -0,0 +1,106 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: workflowtemplates.kubeclaw.scoble.me + labels: + app.kubernetes.io/name: kubeclaw + app.kubernetes.io/part-of: scobleclaw +spec: + group: kubeclaw.scoble.me + names: + kind: WorkflowTemplate + listKind: WorkflowTemplateList + plural: workflowtemplates + singular: workflowtemplate + shortNames: + - wt + - wft + scope: Cluster + versions: + - name: v1alpha1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - name: Audit + type: string + jsonPath: .spec.auditLevel + - name: Steps + type: integer + jsonPath: .spec.steps + - name: Ready + type: boolean + jsonPath: .status.ready + - name: Age + type: date + jsonPath: .metadata.creationTimestamp + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + required: [displayName, description, steps, parametersSchema, outputSchema, auditLevel] + properties: + displayName: + type: string + description: + type: string + steps: + type: array + items: + type: object + required: [name, action, target] + properties: + name: + type: string + action: + type: string + target: + type: string + condition: + type: string + onFailure: + type: string + enum: [stop, skip, continue] + parametersSchema: + type: object + x-kubernetes-preserve-unknown-fields: true + outputSchema: + type: object + x-kubernetes-preserve-unknown-fields: true + complianceRequirements: + type: array + items: + type: string + requiresApproval: + type: array + items: + type: string + allowedDataSources: + type: array + items: + type: string + allowedDestinations: + type: array + items: + type: string + auditLevel: + type: string + enum: [minimal, standard, full] + status: + type: object + properties: + ready: + type: boolean + validated: + type: boolean + usedByAgents: + type: array + items: + type: string + lastInstantiated: + type: string + format: date-time diff --git a/config/deploy/controller.yaml b/config/deploy/controller.yaml new file mode 100644 index 0000000..168cb56 --- /dev/null +++ b/config/deploy/controller.yaml @@ -0,0 +1,49 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kubeclaw-controller + namespace: scobleclaw + labels: + app: kubeclaw + component: controller +spec: + replicas: 1 + selector: + matchLabels: + app: kubeclaw + component: controller + template: + metadata: + labels: + app: kubeclaw + component: controller + spec: + serviceAccountName: kubeclaw-controller + containers: + - name: manager + image: kubeclaw/controller:latest + imagePullPolicy: Never + args: + - --health-probe-bind-address=:8081 + - --metrics-bind-address=:8080 + ports: + - name: metrics + containerPort: 8080 + - name: health + containerPort: 8081 + livenessProbe: + httpGet: + path: /healthz + port: health + readinessProbe: + httpGet: + path: /readyz + port: health + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 256Mi \ No newline at end of file diff --git a/config/deploy/rbac.yaml b/config/deploy/rbac.yaml new file mode 100644 index 0000000..1990ca5 --- /dev/null +++ b/config/deploy/rbac.yaml @@ -0,0 +1,46 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: kubeclaw-controller + namespace: scobleclaw +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: kubeclaw-controller +rules: + - apiGroups: ["kubeclaw.scoble.me"] + resources: ["agents", "agentconfigs", "agentgroups", "channels", "providers", + "workflowtemplates", "compliancepolicies"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + - apiGroups: ["kubeclaw.scoble.me"] + resources: ["agents/status", "agentconfigs/status", "agentgroups/status", + "channels/status", "providers/status", + "workflowtemplates/status", "compliancepolicies/status"] + verbs: ["get", "update", "patch"] + - apiGroups: ["kubeclaw.scoble.me"] + resources: ["agents/finalizers"] + verbs: ["update"] + - apiGroups: [""] + resources: ["pods", "configmaps", "serviceaccounts"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list", "watch"] + - apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: kubeclaw-controller +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kubeclaw-controller +subjects: + - kind: ServiceAccount + name: kubeclaw-controller + namespace: scobleclaw \ No newline at end of file diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml new file mode 100644 index 0000000..a711aa5 --- /dev/null +++ b/config/rbac/role.yaml @@ -0,0 +1,40 @@ +{{- if .Values.rbac.create }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "kubeclaw.fullname" . }} + labels: {{- include "kubeclaw.labels" . | nindent 4 }} +rules: + - apiGroups: ["kubeclaw.scoble.me"] + resources: ["agents", "agentconfigs", "agentgroups", "channels", "providers"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + - apiGroups: ["kubeclaw.scoble.me"] + resources: ["workflowtemplates", "compliancepolicies"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + - apiGroups: ["kubeclaw.scoble.me"] + resources: ["agents/status", "agentconfigs/status", "agentgroups/status", "channels/status", "providers/status", "workflowtemplates/status", "compliancepolicies/status"] + verbs: ["get", "update", "patch"] + - apiGroups: ["kubeclaw.scoble.me"] + resources: ["agents/finalizers"] + verbs: ["update"] + - apiGroups: [""] + resources: ["pods", "configmaps", "serviceaccounts"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "kubeclaw.fullname" . }} + labels: {{- include "kubeclaw.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "kubeclaw.fullname" . }} +subjects: + - kind: ServiceAccount + name: {{ include "kubeclaw.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/config/samples/01-provider.yaml b/config/samples/01-provider.yaml new file mode 100644 index 0000000..4d3896d --- /dev/null +++ b/config/samples/01-provider.yaml @@ -0,0 +1,13 @@ +# Example: LLM provider pointing to a local Ollama instance +apiVersion: kubeclaw.scoble.me/v1alpha1 +kind: Provider +metadata: + name: ollama-cloud +spec: + type: ollama + baseUrl: https://ollama.example.com + models: + - id: deepseek-v4-pro:cloud + default: true + - id: deepseek-v4-flash:cloud + - id: qwen3-coder-next:cloud diff --git a/config/samples/02-channel.yaml b/config/samples/02-channel.yaml new file mode 100644 index 0000000..a0b0c8e --- /dev/null +++ b/config/samples/02-channel.yaml @@ -0,0 +1,11 @@ +apiVersion: kubeclaw.scoble.me/v1alpha1 +kind: Channel +metadata: + name: discord-main +spec: + type: discord + botTokenSecretRef: + name: discord-token + key: token + guildID: "1169414363476672584" + defaultChannel: "1496038116128198676" diff --git a/config/samples/03-agentconfig.yaml b/config/samples/03-agentconfig.yaml new file mode 100644 index 0000000..b9e2ae3 --- /dev/null +++ b/config/samples/03-agentconfig.yaml @@ -0,0 +1,11 @@ +apiVersion: kubeclaw.scoble.me/v1alpha1 +kind: AgentConfig +metadata: + name: default-config +spec: + model: deepseek-v4-pro:cloud + thinking: "off" + heartbeat: + intervalSeconds: 1800 + sandbox: + enabled: false \ No newline at end of file diff --git a/config/samples/04-workflowtemplate.yaml b/config/samples/04-workflowtemplate.yaml new file mode 100644 index 0000000..7964628 --- /dev/null +++ b/config/samples/04-workflowtemplate.yaml @@ -0,0 +1,40 @@ +apiVersion: kubeclaw.scoble.me/v1alpha1 +kind: WorkflowTemplate +metadata: + name: search-memory +spec: + displayName: "Search Memory" + description: "Search the agent's memory store for relevant past context" + steps: + - name: search + action: search_memory + target: memory_store + - name: format-results + action: format_output + target: stdout + parametersSchema: + type: object + properties: + query: + type: string + description: "Search query" + maxResults: + type: integer + default: 5 + required: ["query"] + outputSchema: + type: object + properties: + results: + type: array + items: + type: object + properties: + path: + type: string + content: + type: string + score: + type: number + auditLevel: standard + complianceRequirements: ["SOC2"] diff --git a/config/samples/05-compliancepolicy.yaml b/config/samples/05-compliancepolicy.yaml new file mode 100644 index 0000000..aef27ae --- /dev/null +++ b/config/samples/05-compliancepolicy.yaml @@ -0,0 +1,18 @@ +apiVersion: kubeclaw.scoble.me/v1alpha1 +kind: CompliancePolicy +metadata: + name: soc2-data-access +spec: + displayName: "SOC2 Data Access Policy" + framework: SOC2 + rules: + - name: data-source-check + description: "Ensure data sources are in the allowed list" + severity: block + constraint: "data_source in allowed_sources" + - name: audit-trail + description: "Require full audit trail for data writes" + severity: warn + constraint: "audit_level >= standard" + escalation: + mode: auto_deny diff --git a/config/samples/06-agent.yaml b/config/samples/06-agent.yaml new file mode 100644 index 0000000..3e065d4 --- /dev/null +++ b/config/samples/06-agent.yaml @@ -0,0 +1,25 @@ +apiVersion: kubeclaw.scoble.me/v1alpha1 +kind: Agent +metadata: + name: main-agent + labels: + team: core +spec: + agentId: main + model: deepseek-v4-pro:cloud + agentConfigRef: default-config + providers: + - ollama-cloud + channelRefs: + - discord-main + allowedTemplates: + - search-memory + allowedDataSources: + - memory_store + - vault + allowedDestinations: + - stdout + - discord + compliancePolicyRefs: + - soc2-data-access + replicas: 1 diff --git a/config/samples/07-agentgroup.yaml b/config/samples/07-agentgroup.yaml new file mode 100644 index 0000000..c9a2520 --- /dev/null +++ b/config/samples/07-agentgroup.yaml @@ -0,0 +1,20 @@ +apiVersion: kubeclaw.scoble.me/v1alpha1 +kind: AgentGroup +metadata: + name: scobleclaw-team +spec: + sharedProviders: + - ollama-cloud + sharedChannels: + - discord-main + members: + - name: cos + agentId: main + model: deepseek-v4-pro:cloud + - name: engineering + agentId: engineering + model: qwen3-coder-next:cloud + - name: trust + agentId: trust + model: deepseek-v4-flash:cloud + thinking: low diff --git a/go.mod b/go.mod index f958a72..e986340 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,67 @@ module github.com/sscoble/kubeclaw -go 1.24 +go 1.24.0 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 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/onsi/ginkgo/v2 v2.23.3 // indirect + github.com/onsi/gomega v1.37.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/oauth2 v0.27.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/term v0.30.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/time v0.9.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.33.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d433f4d --- /dev/null +++ b/go.sum @@ -0,0 +1,196 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0= +github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= +github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= +github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU= +k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM= +k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs= +k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc= +k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= +k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98= +k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= +sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt new file mode 100644 index 0000000..45b2ce6 --- /dev/null +++ b/hack/boilerplate.go.txt @@ -0,0 +1,4 @@ +/* +Copyright 2025 Scott Scoble. +Licensed under the MIT License. +*/ diff --git a/internal/controller/agent_controller.go b/internal/controller/agent_controller.go new file mode 100644 index 0000000..24ac0fd --- /dev/null +++ b/internal/controller/agent_controller.go @@ -0,0 +1,190 @@ +/* +Copyright 2025 Scott Scoble. Licensed under the MIT License. +*/ + +package controller + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + + kubeclawv1alpha1 "github.com/sscoble/kubeclaw/api/v1alpha1" +) + +// AgentReconciler reconciles Agent objects. +type AgentReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +func (r *AgentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + logger.Info("reconciling Agent", "name", req.Name, "namespace", req.Namespace) + + agent := &kubeclawv1alpha1.Agent{} + if err := r.Get(ctx, req.NamespacedName, agent); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // Handle deletion + if !agent.DeletionTimestamp.IsZero() { + if controllerutil.ContainsFinalizer(agent, "kubeclaw.scoble.me/finalizer") { + podName := fmt.Sprintf("kubeclaw-agent-%s", agent.Spec.AgentID) + pod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: podName, Namespace: req.Namespace}} + if err := r.Delete(ctx, pod); err != nil && !errors.IsNotFound(err) { + return ctrl.Result{}, err + } + controllerutil.RemoveFinalizer(agent, "kubeclaw.scoble.me/finalizer") + if err := r.Update(ctx, agent); err != nil { + return ctrl.Result{}, err + } + } + return ctrl.Result{}, nil + } + + // Ensure finalizer + if !controllerutil.ContainsFinalizer(agent, "kubeclaw.scoble.me/finalizer") { + controllerutil.AddFinalizer(agent, "kubeclaw.scoble.me/finalizer") + if err := r.Update(ctx, agent); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{Requeue: true}, nil + } + + // Resolve AgentConfig + if agent.Spec.AgentConfigRef != "" { + acfg := &kubeclawv1alpha1.AgentConfig{} + if err := r.Get(ctx, types.NamespacedName{Name: agent.Spec.AgentConfigRef, Namespace: req.Namespace}, acfg); err != nil { + logger.Error(err, "cannot fetch AgentConfig", "ref", agent.Spec.AgentConfigRef) + agent.Status.Phase = "ConfigError" + _ = r.Status().Update(ctx, agent) + return ctrl.Result{}, err + } + if agent.Spec.Model == "" && acfg.Spec.Model != "" { + agent.Spec.Model = acfg.Spec.Model + } + if agent.Spec.Thinking == "" && acfg.Spec.Thinking != "" { + agent.Spec.Thinking = acfg.Spec.Thinking + } + } + + image := agent.Spec.Image + if image == "" { + image = "ghcr.io/sscoble/kubeclaw-agent:latest" + } + + podName := fmt.Sprintf("kubeclaw-agent-%s", agent.Spec.AgentID) + + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: podName, + Namespace: req.Namespace, + Labels: map[string]string{ + "app.kubernetes.io/name": "kubeclaw-agent", + "app.kubernetes.io/instance": podName, + "app.kubernetes.io/part-of": "scobleclaw", + "kubeclaw.scoble.me/agent-id": agent.Spec.AgentID, + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "agent", + Image: image, + Env: r.buildEnv(agent), + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("128Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("500m"), + corev1.ResourceMemory: resource.MustParse("256Mi"), + }, + }, + }}, + }, + } + + // Apply resource overrides + if agent.Spec.Resources != nil { + if cpu, ok := agent.Spec.Resources.Requests["cpu"]; ok { + if q, err := resource.ParseQuantity(cpu); err == nil { + pod.Spec.Containers[0].Resources.Requests[corev1.ResourceCPU] = q + } + } + if mem, ok := agent.Spec.Resources.Requests["memory"]; ok { + if q, err := resource.ParseQuantity(mem); err == nil { + pod.Spec.Containers[0].Resources.Requests[corev1.ResourceMemory] = q + } + } + if cpu, ok := agent.Spec.Resources.Limits["cpu"]; ok { + if q, err := resource.ParseQuantity(cpu); err == nil { + pod.Spec.Containers[0].Resources.Limits[corev1.ResourceCPU] = q + } + } + if mem, ok := agent.Spec.Resources.Limits["memory"]; ok { + if q, err := resource.ParseQuantity(mem); err == nil { + pod.Spec.Containers[0].Resources.Limits[corev1.ResourceMemory] = q + } + } + } + + // Create or update + foundPod := &corev1.Pod{} + err := r.Get(ctx, types.NamespacedName{Name: podName, Namespace: req.Namespace}, foundPod) + if err != nil && errors.IsNotFound(err) { + logger.Info("creating Pod", "name", podName) + if err := r.Create(ctx, pod); err != nil { + return ctrl.Result{}, err + } + } else if err == nil { + foundPod.Spec = pod.Spec + foundPod.Labels = pod.Labels + if err := r.Update(ctx, foundPod); err != nil { + return ctrl.Result{}, err + } + } + + agent.Status.Phase = "Running" + agent.Status.ReadyReplicas = 1 + if err := r.Status().Update(ctx, agent); err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +func (r *AgentReconciler) buildEnv(agent *kubeclawv1alpha1.Agent) []corev1.EnvVar { + env := []corev1.EnvVar{{Name: "KUBECLAW_AGENT_ID", Value: agent.Spec.AgentID}} + if agent.Spec.Model != "" { + env = append(env, corev1.EnvVar{Name: "KUBECLAW_MODEL", Value: agent.Spec.Model}) + } + if agent.Spec.Thinking != "" { + env = append(env, corev1.EnvVar{Name: "KUBECLAW_THINKING", Value: agent.Spec.Thinking}) + } + if agent.Spec.DefaultTemplate != "" { + env = append(env, corev1.EnvVar{Name: "KUBECLAW_DEFAULT_TEMPLATE", Value: agent.Spec.DefaultTemplate}) + } + if len(agent.Spec.AllowedTemplates) > 0 { + env = append(env, corev1.EnvVar{Name: "KUBECLAW_ALLOWED_TEMPLATES", Value: joinSlice(agent.Spec.AllowedTemplates, ",")}) + } + return env +} + +func (r *AgentReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&kubeclawv1alpha1.Agent{}). + Owns(&corev1.Pod{}). + Complete(r) +} diff --git a/internal/controller/compliancepolicy_controller.go b/internal/controller/compliancepolicy_controller.go new file mode 100644 index 0000000..3ae740e --- /dev/null +++ b/internal/controller/compliancepolicy_controller.go @@ -0,0 +1,52 @@ +/* +Copyright 2025 Scott Scoble. Licensed under the MIT License. +*/ + +package controller + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + kubeclawv1alpha1 "github.com/sscoble/kubeclaw/api/v1alpha1" +) + +// CompliancePolicyReconciler reconciles CompliancePolicy objects. +type CompliancePolicyReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +func (r *CompliancePolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + cp := &kubeclawv1alpha1.CompliancePolicy{} + if err := r.Get(ctx, req.NamespacedName, cp); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + logger.Info("validating compliance policy", "name", cp.Name) + + valid := true + if len(cp.Spec.Rules) == 0 { + valid = false + } + + cp.Status.Ready = valid + + if err := r.Status().Update(ctx, cp); err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +func (r *CompliancePolicyReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&kubeclawv1alpha1.CompliancePolicy{}). + Complete(r) +} diff --git a/internal/controller/helpers.go b/internal/controller/helpers.go new file mode 100644 index 0000000..d5a76f4 --- /dev/null +++ b/internal/controller/helpers.go @@ -0,0 +1,12 @@ +package controller + +func joinSlice(items []string, sep string) string { + result := "" + for i, item := range items { + if i > 0 { + result += sep + } + result += item + } + return result +} diff --git a/internal/controller/workflowtemplate_controller.go b/internal/controller/workflowtemplate_controller.go new file mode 100644 index 0000000..735c030 --- /dev/null +++ b/internal/controller/workflowtemplate_controller.go @@ -0,0 +1,57 @@ +/* +Copyright 2025 Scott Scoble. Licensed under the MIT License. +*/ + +package controller + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + kubeclawv1alpha1 "github.com/sscoble/kubeclaw/api/v1alpha1" +) + +// WorkflowTemplateReconciler reconciles WorkflowTemplate objects. +type WorkflowTemplateReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +func (r *WorkflowTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + wt := &kubeclawv1alpha1.WorkflowTemplate{} + if err := r.Get(ctx, req.NamespacedName, wt); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + logger.Info("validating workflow template", "name", wt.Name) + + // Validate basic shape + valid := true + if len(wt.Spec.Steps) == 0 { + valid = false + } + if wt.Spec.AuditLevel == "" { + wt.Spec.AuditLevel = "standard" + } + + wt.Status.Ready = valid + wt.Status.Validated = true + + if err := r.Status().Update(ctx, wt); err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +func (r *WorkflowTemplateReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&kubeclawv1alpha1.WorkflowTemplate{}). + Complete(r) +} diff --git a/manager b/manager new file mode 100755 index 0000000..18fe7c3 Binary files /dev/null and b/manager differ diff --git a/nix/build-image.nix b/nix/build-image.nix new file mode 100644 index 0000000..2550b44 --- /dev/null +++ b/nix/build-image.nix @@ -0,0 +1,35 @@ +# build-image.nix — Package the pre-built kubeclaw controller binary as an OCI image +# Usage: nix-build build-image.nix && result | k3s ctr images import - + +{ pkgs ? import {} }: + +let + manager-bin = pkgs.stdenv.mkDerivation { + name = "kubeclaw-manager"; + src = ./../manager; + phases = [ "installPhase" ]; + installPhase = '' + mkdir -p $out/bin + cp $src $out/bin/manager + chmod +x $out/bin/manager + ''; + }; + +in pkgs.dockerTools.streamLayeredImage { + name = "kubeclaw/controller"; + tag = "latest"; + created = "now"; + contents = [ + manager-bin + pkgs.cacert + pkgs.tzdata + ]; + config = { + User = "65532:65532"; + Entrypoint = [ "/bin/manager" ]; + ExposedPorts = { + "8080/tcp" = {}; + "8081/tcp" = {}; + }; + }; +} \ No newline at end of file diff --git a/nix/result b/nix/result new file mode 120000 index 0000000..5db813c --- /dev/null +++ b/nix/result @@ -0,0 +1 @@ +/nix/store/6xkkvlii8k7jj9i2zj0bkw5xj6dbmz81-stream-controller \ No newline at end of file diff --git a/quickstart.sh b/quickstart.sh new file mode 100755 index 0000000..eda7b13 --- /dev/null +++ b/quickstart.sh @@ -0,0 +1,170 @@ +#!/usr/bin/env bash +set -euo pipefail + +# kubeclaw quickstart: install CRDs + deploy controller on Docker Desktop Kubernetes +# Prerequisites: Docker Desktop with Kubernetes enabled, kubectl pointing to docker-desktop + +echo "=== kubeclaw Quickstart ===" +echo "" + +# 1. Verify cluster access +echo "→ Checking cluster connection..." +kubectl cluster-info --request-timeout=5s > /dev/null 2>&1 || { + echo "ERROR: Cannot reach Kubernetes cluster." + echo "Make sure Docker Desktop has Kubernetes enabled (Settings → Kubernetes → Enable Kubernetes)" + exit 1 +} +echo " ✓ Cluster reachable ($(kubectl config current-context))" + +# 2. Install CRDs +echo "" +echo "→ Installing CRDs..." +for crd in config/crd/bases/*.yaml; do + kubectl apply -f "$crd" +done +echo " ✓ CRDs installed" + +# Wait for CRDs to register +sleep 2 + +# 3. Verify CRDs are ready +echo "" +echo "→ Verifying CRDs..." +kubectl get crd | grep kubeclaw.scoble.me | awk '{print " " $1}' + +# 4. Create namespace +echo "" +echo "→ Creating scobleclaw namespace..." +kubectl create namespace scobleclaw --dry-run=client -o yaml | kubectl apply -f - +echo " ✓ Namespace ready" + +# 5. Deploy controller +echo "" +echo "→ Deploying kubeclaw controller..." +kubectl apply -f config/rbac/role.yaml 2>/dev/null || { + # Generate minimal RBAC if role.yaml doesn't exist + kubectl apply -f - <<'RBAC' +apiVersion: v1 +kind: ServiceAccount +metadata: + name: kubeclaw-controller + namespace: scobleclaw +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: kubeclaw-controller +rules: + - apiGroups: ["kubeclaw.scoble.me"] + resources: ["*"] + verbs: ["*"] + - apiGroups: [""] + resources: ["pods", "configmaps", "serviceaccounts"] + verbs: ["*"] + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list", "watch"] + - apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["*"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: kubeclaw-controller +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kubeclaw-controller +subjects: + - kind: ServiceAccount + name: kubeclaw-controller + namespace: scobleclaw +RBAC +} + +kubectl apply -f - <<'DEPLOYMENT' +apiVersion: v1 +kind: ConfigMap +metadata: + name: kubeclaw-config + namespace: scobleclaw +data: + config.yaml: | + logLevel: info +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kubeclaw-controller + namespace: scobleclaw + labels: + app: kubeclaw +spec: + replicas: 1 + selector: + matchLabels: + app: kubeclaw + template: + metadata: + labels: + app: kubeclaw + spec: + serviceAccountName: kubeclaw-controller + containers: + - name: manager + image: ghcr.io/sscoble/kubeclaw:latest + imagePullPolicy: IfNotPresent + command: ["/manager"] + args: + - --health-probe-bind-address=:8081 + - --metrics-bind-address=:8080 + ports: + - containerPort: 8080 + name: metrics + - containerPort: 8081 + name: health + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 256Mi + livenessProbe: + httpGet: + path: /healthz + port: health + readinessProbe: + httpGet: + path: /readyz + port: health +DEPLOYMENT + +echo " ✓ Controller deployed" + +# 6. Wait for controller +echo "" +echo "→ Waiting for controller to start..." +kubectl wait --for=condition=available deployment/kubeclaw-controller -n scobleclaw --timeout=60s 2>/dev/null || { + echo " (controller may still be starting — check with 'kubectl get pods -n scobleclaw')" +} + +# 7. Apply samples +echo "" +echo "→ Installing sample resources..." +for sample in config/samples/0*.yaml; do + echo " Applying $sample..." + kubectl apply -f "$sample" -n scobleclaw 2>/dev/null || kubectl apply -f "$sample" +done + +echo "" +echo "=== Quickstart Complete ===" +echo "" +echo "Check status:" +echo " kubectl get agents -n scobleclaw" +echo " kubectl get workflowtemplates" +echo " kubectl get compliancepolicies" +echo "" +echo "View controller logs:" +echo " kubectl logs -n scobleclaw deployment/kubeclaw-controller"