Compare commits

..

1 commit

Author SHA1 Message Date
9e2cc0473c fix: agent controller pod reconciliation — use Create not Update, wire volumes/env/VolumeMounts
- Pods are now created fresh on first reconcile instead of trying to update
  existing pods (which fails because Update strips API-injected fields like
  volumes, serviceAccountName, and nodeName)
- Added ImagePullPolicy: Never for locally-built Nix images
- Wired Volumes and VolumeMounts from Agent spec into pod spec
- Wired user-defined Env vars from Agent spec
- Image changes now handled via delete+recreate instead of pod update
- Added podImageEqual helper to detect when image change requires recreation

Fixes marcus/scobleclaw#10
2026-05-14 13:43:57 -07:00
16 changed files with 3249 additions and 735 deletions

View file

@ -7,6 +7,7 @@ Licensed under the MIT License.
package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@ -63,6 +64,10 @@ type AgentSpec struct {
// +optional
CompliancePolicyRefs []string `json:"compliancePolicyRefs,omitempty"`
// Env defines environment variables to set in the agent container.
// +optional
Env []corev1.EnvVar `json:"env,omitempty"`
// Resources defines compute resource requirements.
// +optional
Resources *ResourceRequirements `json:"resources,omitempty"`
@ -74,6 +79,14 @@ type AgentSpec struct {
// Replicas is the number of agent instances. Defaults to 1.
// +optional
Replicas *int32 `json:"replicas,omitempty"`
// Volumes defines volumes to mount into the agent container.
// +optional
Volumes []corev1.Volume `json:"volumes,omitempty"`
// VolumeMounts defines volume mounts for the agent container.
// +optional
VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"`
}
// AgentWorkspace configures the workspace for the agent.

View file

@ -10,8 +10,8 @@ Licensed under the MIT License.
package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
@ -325,7 +325,7 @@ func (in *AgentGroupStatus) DeepCopyInto(out *AgentGroupStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in))
*out = make([]metav1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
@ -419,6 +419,13 @@ func (in *AgentSpec) DeepCopyInto(out *AgentSpec) {
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Env != nil {
in, out := &in.Env, &out.Env
*out = make([]v1.EnvVar, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Resources != nil {
in, out := &in.Resources, &out.Resources
*out = new(ResourceRequirements)
@ -446,7 +453,7 @@ func (in *AgentStatus) DeepCopyInto(out *AgentStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in))
*out = make([]metav1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
@ -551,7 +558,7 @@ func (in *ChannelSpec) DeepCopyInto(out *ChannelSpec) {
*out = *in
if in.BotTokenSecretRef != nil {
in, out := &in.BotTokenSecretRef, &out.BotTokenSecretRef
*out = new(corev1.SecretKeySelector)
*out = new(v1.SecretKeySelector)
(*in).DeepCopyInto(*out)
}
if in.AdditionalConfig != nil {
@ -912,7 +919,7 @@ func (in *ProviderSpec) DeepCopyInto(out *ProviderSpec) {
*out = *in
if in.APIKeySecretRef != nil {
in, out := &in.APIKeySecretRef, &out.APIKeySecretRef
*out = new(corev1.SecretKeySelector)
*out = new(v1.SecretKeySelector)
(*in).DeepCopyInto(*out)
}
if in.Models != nil {

View file

@ -2,10 +2,9 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.5
name: agentconfigs.kubeclaw.scoble.me
labels:
app.kubernetes.io/name: kubeclaw
app.kubernetes.io/part-of: scobleclaw
spec:
group: kubeclaw.scoble.me
names:
@ -13,98 +12,142 @@ spec:
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:
- additionalPrinterColumns:
- jsonPath: .spec.model
name: DefaultModel
type: string
- jsonPath: .status.ready
name: Ready
type: boolean
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: AgentConfig is a reusable configuration template for agents.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: |-
AgentConfigSpec mirrors OpenClaw's agents.defaults.* configuration block.
It's a reusable template that Agent resources can reference.
properties:
heartbeat:
description: Heartbeat configures the agent heartbeat.
properties:
enabled:
description: Enabled controls the heartbeat.
type: boolean
interval:
description: Interval is the heartbeat check interval.
type: string
toolsAllow:
type: array
items:
prompt:
description: Prompt is the heartbeat trigger prompt.
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: object
memorySearch:
description: MemorySearch configures memory search behavior.
properties:
enabled:
description: Enabled controls memory search availability.
type: boolean
maxResults:
description: MaxResults limits search results.
type: integer
minScore:
description: MinScore is the minimum relevance score.
type: number
type: object
model:
description: Model is the default LLM model.
type: string
sandbox:
description: Sandbox configures the execution sandbox.
properties:
allowedExec:
description: AllowedExec is a list of allowed executable paths.
items:
type: string
type: array
enabled:
description: Enabled controls sandbox containment.
type: boolean
type: object
skills:
description: Skills configures skill loading.
properties:
allowed:
description: Allowed is a list of allowed skill IDs.
items:
type: string
type: array
denied:
description: Denied is a list of denied skill IDs.
items:
type: string
type: array
type: object
thinking:
description: Thinking is the thinking level (off/low/high).
type: string
tools:
description: Tools controls tool availability.
properties:
allow:
description: Allow controls tool allowlist.
items:
type: string
type: array
deny:
description: Deny controls tool denylist.
items:
type: string
type: array
type: object
workspace:
description: Workspace configures the agent workspace.
properties:
dir:
description: Dir is the workspace root directory.
type: string
required:
- dir
type: object
type: object
status:
description: AgentConfigStatus defines the observed state of an AgentConfig.
properties:
ready:
description: Ready indicates the template is valid.
type: boolean
validated:
description: Validated indicates the config was validated.
type: boolean
required:
- ready
- validated
type: object
type: object
served: true
storage: true
subresources:
status: {}

View file

@ -2,10 +2,9 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.5
name: agentgroups.kubeclaw.scoble.me
labels:
app.kubernetes.io/name: kubeclaw
app.kubernetes.io/part-of: scobleclaw
spec:
group: kubeclaw.scoble.me
names:
@ -13,95 +12,245 @@ spec:
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:
- additionalPrinterColumns:
- jsonPath: .status.readyAgents
name: Ready
type: integer
- jsonPath: .status.totalAgents
name: Total
type: integer
- jsonPath: .status.phase
name: Phase
type: string
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: AgentGroup is the Schema for the agentgroups API.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: AgentGroupSpec defines the desired state of an AgentGroup.
properties:
agentConfigRef:
description: AgentConfigRef references an AgentConfig template for
all members.
type: string
agents:
description: Agents lists the agents in this group.
items:
description: AgentGroupMember defines a member of an AgentGroup.
properties:
agentId:
description: AgentID is the OpenClaw agent identifier.
type: string
allowedTemplates:
description: AllowedTemplates overrides the group's template
whitelist for this member.
items:
type: string
agentId:
type: string
model:
type: string
thinking:
type: string
configOverrides:
type: object
type: array
channels:
description: Channels lists the channels this specific agent
should bind to.
items:
description: AgentChannel configures a communication channel
binding.
properties:
model:
type: string
thinking:
type: string
allowedTemplates:
type: array
botTokenSecretRef:
description: BotTokenSecretRef references a Secret key
containing the bot token.
properties:
key:
description: Key within the Secret.
type: string
name:
description: Name of the Secret.
type: string
required:
- key
- name
type: object
channelIds:
description: ChannelIDs is the list of channel IDs this
agent should listen to.
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
type: array
enabled:
description: Enabled controls whether this channel is
active.
type: boolean
type:
description: Type is the channel type (e.g., "discord",
"slack", "telegram").
type: string
required:
- botTokenSecretRef
- type
type: object
type: array
model:
description: Model overrides the default model for this agent.
type: string
name:
description: Name is the Agent resource name.
type: string
required:
- agentId
- name
type: object
type: array
allowedTemplates:
description: AllowedTemplates is a whitelist of workflow templates
for all members.
items:
type: string
type: array
channelRefs:
description: ChannelRefs lists Channel resources for the group.
items:
type: string
type: array
compliancePolicyRefs:
description: CompliancePolicyRefs lists CompliancePolicy resources
for the group.
items:
type: string
type: array
networkPolicy:
description: NetworkPolicy controls inter-agent communication.
properties:
allowEgress:
description: AllowEgress controls outbound network access.
type: boolean
allowInterAgent:
description: AllowInterAgent controls whether agents can communicate
with each other.
type: boolean
allowedHosts:
description: AllowedHosts restricts egress to specific hosts.
items:
type: string
type: array
required:
- allowEgress
- allowInterAgent
type: object
providerRefs:
description: ProviderRefs lists Provider resources for the group.
items:
type: string
type: array
sharedSecrets:
description: SharedSecrets references Secrets shared by all agents
in the group.
items:
type: string
type: array
sharedWorkspaceConfigMap:
description: SharedWorkspace references a ConfigMap with shared workspace
config.
type: string
required:
- agents
type: object
status:
description: AgentGroupStatus defines the observed state of an AgentGroup.
properties:
activeWorkflows:
description: ActiveWorkflows is the count of currently running workflows
across members.
type: integer
conditions:
description: Conditions holds the conditions for this agent group.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
phase:
description: Phase is the overall phase of the agent group.
type: string
readyAgents:
description: ReadyAgents is the count of agents with ready pods.
type: integer
totalAgents:
description: TotalAgents is the total count of agents in the group.
type: integer
type: object
type: object
served: true
storage: true
subresources:
status: {}

File diff suppressed because it is too large Load diff

View file

@ -2,10 +2,9 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.5
name: channels.kubeclaw.scoble.me
labels:
app.kubernetes.io/name: kubeclaw
app.kubernetes.io/part-of: scobleclaw
spec:
group: kubeclaw.scoble.me
names:
@ -13,63 +12,116 @@ spec:
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:
- additionalPrinterColumns:
- jsonPath: .spec.type
name: Type
type: string
- jsonPath: .status.ready
name: Ready
type: boolean
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: Channel is the Schema for the channels API.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: ChannelSpec defines a communication channel binding.
properties:
additionalConfig:
additionalProperties:
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:
description: AdditionalConfig is platform-specific configuration passed
as-is.
type: object
botTokenSecretRef:
description: BotTokenSecretRef references a Secret containing the
bot token.
properties:
key:
description: The key of the secret to select from. Must be a
valid secret key.
type: string
status:
type: object
properties:
ready:
type: boolean
connected:
type: boolean
condition:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
optional:
description: Specify whether the Secret or its key must be defined
type: boolean
required:
- key
type: object
x-kubernetes-map-type: atomic
enabled:
description: Enabled controls whether this channel is active.
type: boolean
guildId:
description: GuildID is the Discord/Telegram group identifier.
type: string
type:
description: Type is the channel platform (e.g., "discord", "slack",
"telegram", "signal").
type: string
webhookUrl:
description: WebhookURL is the incoming webhook URL (for outbound-only
channels).
type: string
required:
- botTokenSecretRef
- type
type: object
status:
description: ChannelStatus defines the observed state of a Channel.
properties:
connectedAgents:
description: ConnectedAgents lists agents currently bound to this
channel.
items:
type: string
lastConnected:
type: string
format: date-time
type: array
connectionState:
description: ConnectionState is the current connection state.
type: string
lastConnected:
description: LastConnected is when the channel was last successfully
connected.
format: date-time
type: string
ready:
description: Ready is true if the channel connection is established.
type: boolean
required:
- ready
type: object
type: object
served: true
storage: true
subresources:
status: {}

View file

@ -2,10 +2,9 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.5
name: compliancepolicies.kubeclaw.scoble.me
labels:
app.kubernetes.io/name: kubeclaw
app.kubernetes.io/part-of: scobleclaw
spec:
group: kubeclaw.scoble.me
names:
@ -13,94 +12,166 @@ spec:
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:
- additionalPrinterColumns:
- jsonPath: .spec.framework
name: Framework
type: string
- jsonPath: .spec.escalation.mode
name: Escalation
type: string
- jsonPath: .status.ready
name: Ready
type: boolean
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: CompliancePolicy is the Schema for the compliancepolicies API.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: CompliancePolicySpec defines a compliance/regulatory policy
for workflow judging.
properties:
displayName:
description: DisplayName is a human-readable name.
type: string
escalation:
description: Escalation configures what happens when a rule is violated.
properties:
humanChannelRef:
description: HumanChannelRef references a Channel resource for
human escalation.
type: string
mode:
description: Mode determines what happens on violation.
enum:
- auto_deny
- human_review
- warn_only
type: string
timeoutSeconds:
description: TimeoutSeconds for human review before auto-deny.
type: integer
required:
- mode
type: object
framework:
description: |-
Framework is the compliance framework (e.g., "SOC2", "HIPAA", "GDPR", "custom").
The judge model uses the framework identity to anchor its evaluation context.
type: string
prompt:
description: |-
Prompt is an optional system prompt that anchors the judge model for this policy.
When set, the judge receives this prompt + the workflow parameters + rules.
When unset, the judge uses a default compliance evaluation prompt.
type: string
rules:
description: Rules are the individual compliance rules the judge evaluates
against.
items:
description: ComplianceRule defines a single compliance rule.
properties:
constraint:
description: |-
Constraint is the rule expression evaluated by the judge.
Examples:
"data_source in allowed_sources"
"destination not in blocked_destinations"
"audit_level >= minimum_required"
The judge evaluates these as part of its verdict.
type: string
description:
description: Description explains the rule.
type: string
name:
description: Name of the rule.
type: string
severity:
description: Severity of the rule.
enum:
- block
- warn
- log
type: string
required:
- constraint
- description
- name
- severity
type: object
type: array
targetSelector:
description: TargetSelector selects which agents/templates this policy
applies to.
properties:
matchLabels:
additionalProperties:
type: string
description: MatchLabels selects by agent/workflow labels.
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:
matchNamespaces:
description: MatchNamespaces selects namespaces.
items:
type: string
enum: [auto_deny, human_review, warn_only]
humanChannelRef:
type: array
matchTemplates:
description: MatchTemplates selects templates by name.
items:
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
type: array
type: object
required:
- displayName
- escalation
- framework
- rules
type: object
status:
description: CompliancePolicyStatus defines the observed state of a CompliancePolicy.
properties:
activeTemplates:
description: ActiveTemplates is the count of templates this policy
applies to.
type: integer
lastJudgment:
description: LastJudgment is the timestamp of the most recent judgment.
format: date-time
type: string
ready:
description: Ready indicates the policy is active.
type: boolean
totalJudgments:
description: TotalJudgments is the total number of judgments rendered
under this policy.
format: int64
type: integer
required:
- ready
type: object
type: object
served: true
storage: true
subresources:
status: {}

View file

@ -2,10 +2,9 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.5
name: providers.kubeclaw.scoble.me
labels:
app.kubernetes.io/name: kubeclaw
app.kubernetes.io/part-of: scobleclaw
spec:
group: kubeclaw.scoble.me
names:
@ -13,74 +12,131 @@ spec:
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:
- additionalPrinterColumns:
- jsonPath: .spec.type
name: Type
type: string
- jsonPath: .status.ready
name: Ready
type: boolean
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: Provider is the Schema for the providers API.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: ProviderSpec defines an LLM provider configuration.
properties:
apiKeySecretRef:
description: APIKeySecretRef references a Secret key containing the
API key.
properties:
key:
description: The key of the secret to select from. Must be a
valid secret key.
type: string
status:
type: object
properties:
ready:
type: boolean
condition:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
optional:
description: Specify whether the Secret or its key must be defined
type: boolean
required:
- key
type: object
x-kubernetes-map-type: atomic
baseUrl:
description: BaseURL is the provider API endpoint.
type: string
extraHeaders:
additionalProperties:
type: string
lastChecked:
type: string
format: date-time
description: ExtraHeaders are additional HTTP headers for provider
requests.
type: object
models:
description: Models is the list of model identifiers available through
this provider.
items:
description: ProviderModel defines a single model within a provider.
properties:
cost:
description: Cost configures pricing for this model.
properties:
input:
description: Input is the cost per million input tokens.
type: number
output:
description: Output is the cost per million output tokens.
type: number
required:
- input
- output
type: object
default:
description: Default is true if this is the default model for
the provider.
type: boolean
id:
description: ID is the model identifier (e.g., "deepseek-v4-pro:cloud").
type: string
required:
- id
type: object
type: array
type:
description: Type is the provider type (e.g., "ollama", "openai",
"anthropic", "deepseek").
type: string
required:
- type
type: object
status:
description: ProviderStatus defines the observed state of a Provider.
properties:
condition:
description: Condition is the current condition of the provider.
type: string
lastChecked:
description: LastChecked is when the provider was last validated.
format: date-time
type: string
ready:
description: Ready is true if the provider is reachable and configured.
type: boolean
required:
- ready
type: object
type: object
served: true
storage: true
subresources:
status: {}

View file

@ -2,10 +2,9 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.5
name: workflowtemplates.kubeclaw.scoble.me
labels:
app.kubernetes.io/name: kubeclaw
app.kubernetes.io/part-of: scobleclaw
spec:
group: kubeclaw.scoble.me
names:
@ -13,94 +12,156 @@ spec:
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:
- additionalPrinterColumns:
- jsonPath: .spec.auditLevel
name: Audit
type: string
- jsonPath: .spec.steps|length
name: Steps
type: integer
- jsonPath: .status.ready
name: Ready
type: boolean
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: WorkflowTemplate is the Schema for the workflowtemplates API.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: WorkflowTemplateSpec defines a workflow template that agents
can instantiate.
properties:
allowedDataSources:
description: AllowedDataSources restricts which sources this workflow
can read from.
items:
type: string
description:
type: array
allowedDestinations:
description: AllowedDestinations restricts which sinks this workflow
can write to.
items:
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: array
auditLevel:
description: AuditLevel controls how thoroughly this workflow is audited.
enum:
- minimal
- standard
- full
type: string
complianceRequirements:
description: ComplianceRequirements lists compliance frameworks this
template satisfies.
items:
type: string
type: array
description:
description: Description explains what this workflow does.
type: string
displayName:
description: DisplayName is a human-readable name.
type: string
outputSchema:
description: OutputSchema defines the expected output shape (JSON
Schema).
type: object
x-kubernetes-preserve-unknown-fields: true
parametersSchema:
description: ParametersSchema defines the JSON Schema for allowed
parameters.
type: object
x-kubernetes-preserve-unknown-fields: true
requiresApproval:
description: RequiresApproval lists gates that require explicit approval.
items:
type: string
type: array
steps:
description: Steps defines the workflow steps/actions.
items:
description: WorkflowStep defines a single step within a workflow.
properties:
action:
description: |-
Action is the operation this step performs.
Examples: "read_file", "search_memory", "send_message", "write_vault"
type: string
condition:
description: Condition is an optional condition for executing
this step.
type: string
name:
description: Name identifies the step within the workflow.
type: string
onFailure:
description: OnFailure controls behavior when this step fails.
enum:
- stop
- skip
- continue
type: string
target:
description: Target is the step target (varies by action).
type: string
required:
- action
- name
- target
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: array
required:
- auditLevel
- description
- displayName
- steps
type: object
status:
description: WorkflowTemplateStatus defines the observed state of a WorkflowTemplate.
properties:
lastInstantiated:
description: LastInstantiated is when this template was last run.
format: date-time
type: string
ready:
description: Ready indicates the template is valid and usable.
type: boolean
usedByAgents:
description: UsedByAgents lists agents currently using this template.
items:
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
type: array
validated:
description: Validated indicates the template schema was validated.
type: boolean
required:
- ready
- validated
type: object
type: object
served: true
storage: true
subresources:
status: {}

View file

@ -1,11 +0,0 @@
apiVersion: kubeclaw.scoble.me/v1alpha1
kind: Channel
metadata:
name: discord-demo
spec:
type: discord
botTokenSecretRef:
name: channel-tokens
key: discord-token
guildID: "1169414363476672584"
defaultChannel: "1496038116128198676"

View file

@ -1,64 +0,0 @@
apiVersion: kubeclaw.scoble.me/v1alpha1
kind: Agent
metadata:
name: demo-concierge
namespace: cust-demo
labels:
customer: demo-corp
kubeclaw.scoble.me/role: concierge
workspace: demo-workspace
spec:
agentId: demo-concierge
model: deepseek-v4-pro:cloud
image: hermes-speckl/sme:latest
replicas: 1
resources:
limits:
cpu: "0.5"
memory: 512Mi
requests:
cpu: "0.25"
memory: 256Mi
channelRefs:
- discord-demo
allowedDestinations:
- stdout
- discord
env:
- name: HERMES_MODEL_PROVIDER
value: "ollama"
- name: HERMES_MODEL_BASE_URL
value: "https://api.ollama.cloud/v1"
- name: HERMES_MODEL_DEFAULT
value: "deepseek-v4-pro:cloud"
- name: HERMES_LOG_LEVEL
value: "debug"
- name: DISCORD_BOT_TOKEN
valueFrom:
secretKeyRef:
name: channel-tokens
key: discord-token
- name: GATEWAY_ALLOW_ALL_USERS
value: "true"
# Volume mounts for persona files (optional, set after personalization)
# volumes:
# - name: soul
# configMap:
# name: demo-concierge-soul
# - name: agents
# configMap:
# name: demo-concierge-agents
# - name: memory
# emptyDir:
# sizeLimit: 1Gi
# volumeMounts:
# - name: soul
# mountPath: /home/hermes/.hermes/persona/SOUL.md
# subPath: SOUL.md
# readOnly: true
# - name: agents
# mountPath: /home/hermes/.hermes/persona/AGENTS.md
# subPath: AGENTS.md
# readOnly: true
# - name: memory
# mountPath: /home/hermes/.hermes/memory

View file

@ -2,6 +2,7 @@ apiVersion: kubeclaw.scoble.me/v1alpha1
kind: Agent
metadata:
name: main-agent
namespace: scobleclaw
labels:
team: core
spec:
@ -23,3 +24,34 @@ spec:
compliancePolicyRefs:
- soc2-data-access
replicas: 1
image: hermes-speckl/sme:latest
volumes:
- name: soul
configMap:
name: hermes-speckl-soul
- name: agents
configMap:
name: hermes-speckl-agents
- name: memory
emptyDir:
sizeLimit: 1Gi
volumeMounts:
- name: soul
mountPath: /home/hermes/.hermes/persona/SOUL.md
subPath: SOUL.md
readOnly: true
- name: agents
mountPath: /home/hermes/.hermes/persona/AGENTS.md
subPath: AGENTS.md
readOnly: true
- name: memory
mountPath: /home/hermes/.hermes/memory
env:
- name: HERMES_MODEL_PROVIDER
value: "ollama"
- name: HERMES_MODEL_BASE_URL
value: "https://api.ollama.cloud/v1"
- name: HERMES_MODEL_DEFAULT
value: "deepseek-v4-pro:cloud"
- name: HERMES_LOG_LEVEL
value: "debug"

View file

@ -1,43 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: kubeclaw-agent-demo-concierge
namespace: cust-demo
labels:
app.kubernetes.io/name: kubeclaw-agent
app.kubernetes.io/instance: kubeclaw-agent-demo-concierge
app.kubernetes.io/part-of: scobleclaw
kubeclaw.scoble.me/agent-id: demo-concierge
spec:
containers:
- name: agent
image: hermes-speckl/sme:latest
imagePullPolicy: Never
env:
- name: KUBECLAW_AGENT_ID
value: demo-concierge
- name: KUBECLAW_MODEL
value: deepseek-v4-pro:cloud
- name: HERMES_MODEL_PROVIDER
value: "ollama"
- name: HERMES_MODEL_BASE_URL
value: "https://api.ollama.cloud/v1"
- name: HERMES_MODEL_DEFAULT
value: "deepseek-v4-pro:cloud"
- name: HERMES_LOG_LEVEL
value: "debug"
- name: DISCORD_BOT_TOKEN
valueFrom:
secretKeyRef:
name: channel-tokens
key: discord-token
- name: GATEWAY_ALLOW_ALL_USERS
value: "true"
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
restartPolicy: Always

View file

@ -0,0 +1,12 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: hermes-config
namespace: scobleclaw
data:
config.yaml: |
model:
provider: ollama
base_url: https://api.ollama.cloud/v1
default: deepseek-v4-pro:cloud
log_level: debug

View file

@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: hermes-speckl-memory
namespace: scobleclaw
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi

View file

@ -98,10 +98,13 @@ func (r *AgentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl
},
},
Spec: corev1.PodSpec{
Volumes: agent.Spec.Volumes,
Containers: []corev1.Container{{
Name: "agent",
Image: image,
Env: r.buildEnv(agent),
Name: "agent",
Image: image,
ImagePullPolicy: corev1.PullNever, // locally-built Nix images
Env: r.buildEnv(agent),
VolumeMounts: agent.Spec.VolumeMounts,
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("100m"),
@ -149,11 +152,21 @@ func (r *AgentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl
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
// Determine if we need to update the image (the only mutable field
// on running pods besides activeDeadlineSeconds and tolerations).
// For any other spec changes, delete and recreate.
imageChanged := !podImageEqual(foundPod.Spec.Containers, pod.Spec.Containers)
if imageChanged {
logger.Info("container image changed — deleting pod for full recreation", "name", podName)
if err := r.Delete(ctx, foundPod); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{Requeue: true}, nil
}
// No meaningful changes — nothing to do
logger.V(1).Info("pod up to date", "name", podName)
}
agent.Status.Phase = "Running"
@ -165,6 +178,20 @@ func (r *AgentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl
return ctrl.Result{}, nil
}
// podImageEqual returns true if the container images match between desired and found.
// Image is the only mutable field on a running pod (besides activeDeadlineSeconds and tolerations).
func podImageEqual(foundContainers, desiredContainers []corev1.Container) bool {
if len(foundContainers) != len(desiredContainers) {
return false
}
for i := range foundContainers {
if foundContainers[i].Image != desiredContainers[i].Image {
return false
}
}
return true
}
func (r *AgentReconciler) buildEnv(agent *kubeclawv1alpha1.Agent) []corev1.EnvVar {
env := []corev1.EnvVar{{Name: "KUBECLAW_AGENT_ID", Value: agent.Spec.AgentID}}
if agent.Spec.Model != "" {
@ -179,6 +206,10 @@ func (r *AgentReconciler) buildEnv(agent *kubeclawv1alpha1.Agent) []corev1.EnvVa
if len(agent.Spec.AllowedTemplates) > 0 {
env = append(env, corev1.EnvVar{Name: "KUBECLAW_ALLOWED_TEMPLATES", Value: joinSlice(agent.Spec.AllowedTemplates, ",")})
}
// Append user-defined env vars from Agent spec
for _, e := range agent.Spec.Env {
env = append(env, e)
}
return env
}