Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions api/v1alpha1/agentteam_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@ type AgentTeamSpec struct {
// +kubebuilder:default="claude-code"
// +optional
Harness string `json:"harness,omitempty"`

// ImagePullSecrets are credentials for pulling private container
// images, including OCI-distributed skills. The same secrets are
// applied to agent pods (for the runner image) and to skill-puller
// init containers (for pulling skill artifacts via ORAS). Use
// kubernetes.io/dockerconfigjson Secrets — the operator mounts them
// into the init container so ORAS can resolve registry credentials
// from $DOCKER_CONFIG.
// +optional
ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`
}

// RepositorySpec defines the git repository configuration.
Expand Down Expand Up @@ -127,20 +137,37 @@ type LeadSpec struct {
Resources corev1.ResourceRequirements `json:"resources,omitempty"`
}

// SkillSource identifies where to load a skill from. Exactly one field should be set.
// SkillSource identifies where to load a skill from. Exactly one of
// ConfigMap or OCI must be set (enforced by CEL on SkillSpec).
//
// ConfigMap is simplest and lives entirely within the cluster — good
// for skills authored alongside the team CRs. OCI distributes skills
// as registry artifacts so they can be versioned, signed, shared
// across clusters, and pulled from public or private registries.
type SkillSource struct {
// ConfigMap references a ConfigMap in the same namespace.
// Each key in the ConfigMap becomes a file in the skill directory.
// +optional
ConfigMap string `json:"configMap,omitempty"`

// OCI is an OCI artifact reference containing the skill files (e.g. "ghcr.io/org/skills/web-research:v1").
// TODO: OCI skill pulling is not yet implemented; use ConfigMap instead.
// OCI is an OCI artifact reference containing the skill files
// (e.g. "ghcr.io/org/skills/web-research:v1"). The operator runs
// an `oras pull` init container at pod startup to materialize the
// skill onto an emptyDir; the main container then sees the files
// under ~/.claude/skills/{name}/. Private registries are supported
// via spec.imagePullSecrets.
//
// Re-pull semantics: the init container runs once per pod start,
// so the artifact is re-pulled on every pod create. There is no
// shared cache between pods — operators who want one should pin
// to immutable digests so the registry can short-circuit identical
// pulls cheaply.
// +optional
OCI string `json:"oci,omitempty"`
}

// SkillSpec defines a Claude Code skill to mount into an agent pod.
// +kubebuilder:validation:XValidation:rule="(has(self.source.configMap) ? 1 : 0) + (has(self.source.oci) ? 1 : 0) == 1",message="skill source must set exactly one of configMap or oci"
type SkillSpec struct {
// Name is the skill directory name under .claude/skills/.
Name string `json:"name"`
Expand Down
12 changes: 9 additions & 3 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 18 additions & 2 deletions charts/kagents/crds/kagents.dev_agentteamruns.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -187,14 +187,30 @@ spec:
Each key in the ConfigMap becomes a file in the skill directory.
type: string
oci:
description: OCI is an OCI artifact reference containing
the skill files (e.g. "ghcr.io/org/skills/web-research:v1").
description: |-
OCI is an OCI artifact reference containing the skill files
(e.g. "ghcr.io/org/skills/web-research:v1"). The operator runs
an `oras pull` init container at pod startup to materialize the
skill onto an emptyDir; the main container then sees the files
under ~/.claude/skills/{name}/. Private registries are supported
via spec.imagePullSecrets.

Re-pull semantics: the init container runs once per pod start,
so the artifact is re-pulled on every pod create. There is no
shared cache between pods — operators who want one should pin
to immutable digests so the registry can short-circuit identical
pulls cheaply.
type: string
type: object
required:
- name
- source
type: object
x-kubernetes-validations:
- message: skill source must set exactly one of configMap or
oci
rule: '(has(self.source.configMap) ? 1 : 0) + (has(self.source.oci)
? 1 : 0) == 1'
type: array
required:
- prompt
Expand Down
66 changes: 62 additions & 4 deletions charts/kagents/crds/kagents.dev_agentteams.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,32 @@ spec:
enum:
- claude-code
type: string
imagePullSecrets:
description: |-
ImagePullSecrets are credentials for pulling private container
images, including OCI-distributed skills. The same secrets are
applied to agent pods (for the runner image) and to skill-puller
init containers (for pulling skill artifacts via ORAS). Use
kubernetes.io/dockerconfigjson Secrets — the operator mounts them
into the init container so ORAS can resolve registry credentials
from $DOCKER_CONFIG.
items:
description: |-
LocalObjectReference contains enough information to let you locate the
referenced object inside the same namespace.
properties:
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
type: object
x-kubernetes-map-type: atomic
type: array
lead:
description: Lead configures the team lead agent.
properties:
Expand Down Expand Up @@ -244,14 +270,30 @@ spec:
Each key in the ConfigMap becomes a file in the skill directory.
type: string
oci:
description: OCI is an OCI artifact reference containing
the skill files (e.g. "ghcr.io/org/skills/web-research:v1").
description: |-
OCI is an OCI artifact reference containing the skill files
(e.g. "ghcr.io/org/skills/web-research:v1"). The operator runs
an `oras pull` init container at pod startup to materialize the
skill onto an emptyDir; the main container then sees the files
under ~/.claude/skills/{name}/. Private registries are supported
via spec.imagePullSecrets.

Re-pull semantics: the init container runs once per pod start,
so the artifact is re-pulled on every pod create. There is no
shared cache between pods — operators who want one should pin
to immutable digests so the registry can short-circuit identical
pulls cheaply.
type: string
type: object
required:
- name
- source
type: object
x-kubernetes-validations:
- message: skill source must set exactly one of configMap or
oci
rule: '(has(self.source.configMap) ? 1 : 0) + (has(self.source.oci)
? 1 : 0) == 1'
type: array
required:
- prompt
Expand Down Expand Up @@ -825,14 +867,30 @@ spec:
Each key in the ConfigMap becomes a file in the skill directory.
type: string
oci:
description: OCI is an OCI artifact reference containing
the skill files (e.g. "ghcr.io/org/skills/web-research:v1").
description: |-
OCI is an OCI artifact reference containing the skill files
(e.g. "ghcr.io/org/skills/web-research:v1"). The operator runs
an `oras pull` init container at pod startup to materialize the
skill onto an emptyDir; the main container then sees the files
under ~/.claude/skills/{name}/. Private registries are supported
via spec.imagePullSecrets.

Re-pull semantics: the init container runs once per pod start,
so the artifact is re-pulled on every pod create. There is no
shared cache between pods — operators who want one should pin
to immutable digests so the registry can short-circuit identical
pulls cheaply.
type: string
type: object
required:
- name
- source
type: object
x-kubernetes-validations:
- message: skill source must set exactly one of configMap
or oci
rule: '(has(self.source.configMap) ? 1 : 0) + (has(self.source.oci)
? 1 : 0) == 1'
type: array
required:
- name
Expand Down
20 changes: 18 additions & 2 deletions charts/kagents/crds/kagents.dev_agentteamschedules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -206,14 +206,30 @@ spec:
Each key in the ConfigMap becomes a file in the skill directory.
type: string
oci:
description: OCI is an OCI artifact reference containing
the skill files (e.g. "ghcr.io/org/skills/web-research:v1").
description: |-
OCI is an OCI artifact reference containing the skill files
(e.g. "ghcr.io/org/skills/web-research:v1"). The operator runs
an `oras pull` init container at pod startup to materialize the
skill onto an emptyDir; the main container then sees the files
under ~/.claude/skills/{name}/. Private registries are supported
via spec.imagePullSecrets.

Re-pull semantics: the init container runs once per pod start,
so the artifact is re-pulled on every pod create. There is no
shared cache between pods — operators who want one should pin
to immutable digests so the registry can short-circuit identical
pulls cheaply.
type: string
type: object
required:
- name
- source
type: object
x-kubernetes-validations:
- message: skill source must set exactly one of configMap or
oci
rule: '(has(self.source.configMap) ? 1 : 0) + (has(self.source.oci)
? 1 : 0) == 1'
type: array
required:
- prompt
Expand Down
20 changes: 18 additions & 2 deletions charts/kagents/crds/kagents.dev_agentteamtemplates.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -522,14 +522,30 @@ spec:
Each key in the ConfigMap becomes a file in the skill directory.
type: string
oci:
description: OCI is an OCI artifact reference containing
the skill files (e.g. "ghcr.io/org/skills/web-research:v1").
description: |-
OCI is an OCI artifact reference containing the skill files
(e.g. "ghcr.io/org/skills/web-research:v1"). The operator runs
an `oras pull` init container at pod startup to materialize the
skill onto an emptyDir; the main container then sees the files
under ~/.claude/skills/{name}/. Private registries are supported
via spec.imagePullSecrets.

Re-pull semantics: the init container runs once per pod start,
so the artifact is re-pulled on every pod create. There is no
shared cache between pods — operators who want one should pin
to immutable digests so the registry can short-circuit identical
pulls cheaply.
type: string
type: object
required:
- name
- source
type: object
x-kubernetes-validations:
- message: skill source must set exactly one of configMap
or oci
rule: '(has(self.source.configMap) ? 1 : 0) + (has(self.source.oci)
? 1 : 0) == 1'
type: array
required:
- name
Expand Down
20 changes: 18 additions & 2 deletions charts/kagents/crds/kagents.dev_agentteamtriggers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -217,14 +217,30 @@ spec:
Each key in the ConfigMap becomes a file in the skill directory.
type: string
oci:
description: OCI is an OCI artifact reference containing
the skill files (e.g. "ghcr.io/org/skills/web-research:v1").
description: |-
OCI is an OCI artifact reference containing the skill files
(e.g. "ghcr.io/org/skills/web-research:v1"). The operator runs
an `oras pull` init container at pod startup to materialize the
skill onto an emptyDir; the main container then sees the files
under ~/.claude/skills/{name}/. Private registries are supported
via spec.imagePullSecrets.

Re-pull semantics: the init container runs once per pod start,
so the artifact is re-pulled on every pod create. There is no
shared cache between pods — operators who want one should pin
to immutable digests so the registry can short-circuit identical
pulls cheaply.
type: string
type: object
required:
- name
- source
type: object
x-kubernetes-validations:
- message: skill source must set exactly one of configMap or
oci
rule: '(has(self.source.configMap) ? 1 : 0) + (has(self.source.oci)
? 1 : 0) == 1'
type: array
required:
- prompt
Expand Down
17 changes: 10 additions & 7 deletions cmd/manager/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func main() {
var enableLeaderElection bool
var agentImage string
var initImage string
var skillPullerImage string
var skipInitScript bool
var pvcAccessMode string
var agentCommand string
Expand All @@ -46,6 +47,7 @@ func main() {
flag.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager.")
flag.StringVar(&agentImage, "agent-image", "", "Override the container image used for agent pods (default: ghcr.io/amcheste/claude-code-runner:latest).")
flag.StringVar(&initImage, "init-image", "", "Override the container image used for the repo init Job (default: alpine/git:latest).")
flag.StringVar(&skillPullerImage, "skill-puller-image", "", "Override the container image used to pull OCI-distributed skills (default: ghcr.io/oras-project/oras:v1.2.0). Air-gapped clusters can pin to an internal mirror.")
flag.BoolVar(&skipInitScript, "skip-init-script", false, "Replace the init Job git-clone script with a no-op exit 0. Use in acceptance tests where no real repo is available.")
flag.StringVar(&pvcAccessMode, "pvc-access-mode", "", "Override PVC access mode for all operator-managed PVCs (ReadWriteMany|ReadWriteOnce). Defaults to ReadWriteMany. Set to ReadWriteOnce for single-node clusters like Kind.")
flag.StringVar(&agentCommand, "agent-command", "", "Override the agent container command as a comma-separated list (e.g. sh,-c,sleep 30 && exit 0). Used in acceptance tests to keep pods alive long enough to observe.")
Expand All @@ -71,13 +73,14 @@ func main() {
}

reconciler := &controller.AgentTeamReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
AgentImage: agentImage,
InitImage: initImage,
SkipInitScript: skipInitScript,
Harnesses: harness.DefaultRegistry(),
Delivery: delivery.NewDispatcher(),
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
AgentImage: agentImage,
InitImage: initImage,
SkillPullerImage: skillPullerImage,
SkipInitScript: skipInitScript,
Harnesses: harness.DefaultRegistry(),
Delivery: delivery.NewDispatcher(),
}
if agentCommand != "" {
reconciler.AgentCommand = strings.Split(agentCommand, ",")
Expand Down
Loading
Loading