diff --git a/docker/skills-init/Dockerfile b/docker/skills-init/Dockerfile index e884f34cf..dc89810f7 100644 --- a/docker/skills-init/Dockerfile +++ b/docker/skills-init/Dockerfile @@ -17,7 +17,7 @@ FROM alpine:3.23 ARG PYTHON_UID=1001 ARG PYTHON_GID=1001 -RUN apk upgrade --no-cache && apk add --no-cache git +RUN apk upgrade --no-cache && apk add --no-cache git jq COPY --from=krane-builder /build/krane /usr/local/bin/krane # Run as the same UID/GID as the main agent container (python user) so that diff --git a/go/api/config/crd/bases/kagent.dev_agents.yaml b/go/api/config/crd/bases/kagent.dev_agents.yaml index 1c95c6912..7e36bc1bd 100644 --- a/go/api/config/crd/bases/kagent.dev_agents.yaml +++ b/go/api/config/crd/bases/kagent.dev_agents.yaml @@ -13319,6 +13319,31 @@ spec: maxItems: 20 minItems: 1 type: array + imagePullSecrets: + description: |- + ImagePullSecrets is a list of references to secrets in the same namespace to use for + pulling skill images from private registries. Each referenced secret must be of type + kubernetes.io/dockerconfigjson. The credentials from all secrets are merged and made + available to the skills-init container at /.kagent/.docker/config.json; krane will + use them automatically when pulling images. + 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 + maxItems: 20 + type: array initContainer: description: Configuration for the skills-init init container. properties: diff --git a/go/api/config/crd/bases/kagent.dev_sandboxagents.yaml b/go/api/config/crd/bases/kagent.dev_sandboxagents.yaml index f790af205..5bf46cce1 100644 --- a/go/api/config/crd/bases/kagent.dev_sandboxagents.yaml +++ b/go/api/config/crd/bases/kagent.dev_sandboxagents.yaml @@ -10969,6 +10969,31 @@ spec: maxItems: 20 minItems: 1 type: array + imagePullSecrets: + description: |- + ImagePullSecrets is a list of references to secrets in the same namespace to use for + pulling skill images from private registries. Each referenced secret must be of type + kubernetes.io/dockerconfigjson. The credentials from all secrets are merged and made + available to the skills-init container at /.kagent/.docker/config.json; krane will + use them automatically when pulling images. + 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 + maxItems: 20 + type: array initContainer: description: Configuration for the skills-init init container. properties: diff --git a/go/api/v1alpha2/agent_types.go b/go/api/v1alpha2/agent_types.go index f4c8ded3d..b157b4c9f 100644 --- a/go/api/v1alpha2/agent_types.go +++ b/go/api/v1alpha2/agent_types.go @@ -97,6 +97,15 @@ type SkillForAgent struct { // +optional Refs []string `json:"refs,omitempty"` + // ImagePullSecrets is a list of references to secrets in the same namespace to use for + // pulling skill images from private registries. Each referenced secret must be of type + // kubernetes.io/dockerconfigjson. The credentials from all secrets are merged and made + // available to the skills-init container at /.kagent/.docker/config.json; krane will + // use them automatically when pulling images. + // +optional + // +kubebuilder:validation:MaxItems=20 + ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` + // Reference to a Secret containing git credentials. // Applied to all gitRefs entries. // The secret should contain a `token` key for HTTPS auth, diff --git a/go/api/v1alpha2/zz_generated.deepcopy.go b/go/api/v1alpha2/zz_generated.deepcopy.go index 0d86756c7..724d80c5a 100644 --- a/go/api/v1alpha2/zz_generated.deepcopy.go +++ b/go/api/v1alpha2/zz_generated.deepcopy.go @@ -1423,6 +1423,11 @@ func (in *SkillForAgent) DeepCopyInto(out *SkillForAgent) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.ImagePullSecrets != nil { + in, out := &in.ImagePullSecrets, &out.ImagePullSecrets + *out = make([]v1.LocalObjectReference, len(*in)) + copy(*out, *in) + } if in.GitAuthSecretRef != nil { in, out := &in.GitAuthSecretRef, &out.GitAuthSecretRef *out = new(v1.LocalObjectReference) diff --git a/go/core/internal/controller/translator/agent/adk_api_translator.go b/go/core/internal/controller/translator/agent/adk_api_translator.go index b11d030aa..31d515a12 100644 --- a/go/core/internal/controller/translator/agent/adk_api_translator.go +++ b/go/core/internal/controller/translator/agent/adk_api_translator.go @@ -1214,6 +1214,17 @@ var skillsInitScriptTmpl string // skillsScriptTemplate is the shell script template for fetching skills from Git and OCI. var skillsScriptTemplate = template.Must(template.New("skills-init").Parse(skillsInitScriptTmpl)) +// dockerAuthInitData holds the secret names for the docker-auth-init script template. +type dockerAuthInitData struct { + Secrets []string +} + +//go:embed docker-auth-init.sh.tmpl +var dockerAuthInitScriptTmpl string + +// dockerAuthInitTemplate is the shell script template for merging Docker auth credentials. +var dockerAuthInitTemplate = template.Must(template.New("docker-auth-init").Parse(dockerAuthInitScriptTmpl)) + // buildSkillsScript renders the unified skills-init shell script. func buildSkillsScript(data skillsInitData) (string, error) { var buf bytes.Buffer @@ -1324,6 +1335,9 @@ func prepareSkillsInitData( // buildSkillsInitContainer creates the unified init container and associated volumes // for fetching skills from both Git repositories and OCI registries. // If authSecretRef is non-nil a single Secret volume is created and mounted at /git-auth. +// If imagePullSecrets is non-empty, a docker-auth-init container is prepended that merges +// all kubernetes.io/dockerconfigjson secrets into /.kagent/.docker/config.json; krane +// reads the directory via the DOCKER_CONFIG env var and uses those credentials automatically. func buildSkillsInitContainer( gitRefs []v1alpha2.GitRepo, authSecretRef *corev1.LocalObjectReference, @@ -1332,14 +1346,15 @@ func buildSkillsInitContainer( securityContext *corev1.SecurityContext, env []corev1.EnvVar, resources corev1.ResourceRequirements, -) (container corev1.Container, volumes []corev1.Volume, err error) { + imagePullSecrets []corev1.LocalObjectReference, +) (containers []corev1.Container, volumes []corev1.Volume, err error) { data, err := prepareSkillsInitData(gitRefs, authSecretRef, ociRefs, insecureOCI) if err != nil { - return corev1.Container{}, nil, err + return nil, nil, err } script, err := buildSkillsScript(data) if err != nil { - return corev1.Container{}, nil, err + return nil, nil, err } initSecCtx := securityContext if initSecCtx != nil { @@ -1367,7 +1382,60 @@ func buildSkillsInitContainer( }) } - container = corev1.Container{ + // If imagePullSecrets are specified, build a docker-auth-init container that merges all + // kubernetes.io/dockerconfigjson secrets into a single config.json using jq, then mount + // the result into the skills-init container so krane can authenticate to private registries. + if len(imagePullSecrets) > 0 { + // Shared EmptyDir volume for the merged Docker config. + volumes = append(volumes, corev1.Volume{ + Name: "kagent-docker-config", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }) + + // Mount each imagePullSecret as a read-only directory under /docker-secrets/. + authInitVolumeMounts := []corev1.VolumeMount{ + {Name: "kagent-docker-config", MountPath: "/docker-config-out"}, + } + for _, secret := range imagePullSecrets { + volName := "pull-secret-" + secret.Name + volumes = append(volumes, corev1.Volume{ + Name: volName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: secret.Name, + }, + }, + }) + authInitVolumeMounts = append(authInitVolumeMounts, corev1.VolumeMount{ + Name: volName, + MountPath: "/docker-secrets/" + secret.Name, + ReadOnly: true, + }) + } + + mergeScript, err := buildDockerAuthMergeScript(imagePullSecrets) + if err != nil { + return nil, nil, err + } + dockerAuthInitContainer := corev1.Container{ + Name: "docker-auth-init", + Image: DefaultSkillsInitImageConfig.Image(), + Command: []string{"/bin/sh", "-c", mergeScript}, + VolumeMounts: authInitVolumeMounts, + } + containers = append(containers, dockerAuthInitContainer) + + // Mount the merged config into skills-init so krane picks it up via DOCKER_CONFIG. + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: "kagent-docker-config", + MountPath: "/.kagent/.docker", + ReadOnly: true, + }) + } + + skillsInitContainer := corev1.Container{ Name: "skills-init", Image: DefaultSkillsInitImageConfig.Image(), Command: []string{"/bin/sh", "-c", script}, @@ -1377,7 +1445,31 @@ func buildSkillsInitContainer( Resources: resources, } - return container, volumes, nil + // If a merged Docker config is available, point krane to it via DOCKER_CONFIG. + if len(imagePullSecrets) > 0 { + skillsInitContainer.Env = append(skillsInitContainer.Env, corev1.EnvVar{ + Name: "DOCKER_CONFIG", + Value: "/.kagent/.docker", + }) + } + + containers = append(containers, skillsInitContainer) + return containers, volumes, nil +} + +// buildDockerAuthMergeScript renders the docker-auth-init shell script from the template. +// It merges the .auths sections from all kubernetes.io/dockerconfigjson secrets +// (mounted under /docker-secrets//) into a single config.json using jq. +func buildDockerAuthMergeScript(imagePullSecrets []corev1.LocalObjectReference) (string, error) { + names := make([]string, len(imagePullSecrets)) + for i, s := range imagePullSecrets { + names[i] = s.Name + } + var buf bytes.Buffer + if err := dockerAuthInitTemplate.Execute(&buf, dockerAuthInitData{Secrets: names}); err != nil { + return "", fmt.Errorf("failed to render docker-auth-init script: %w", err) + } + return buf.String(), nil } func (a *adkApiTranslator) runPlugins(ctx context.Context, agent v1alpha2.AgentObject, outputs *AgentOutputs) error { diff --git a/go/core/internal/controller/translator/agent/docker-auth-init.sh.tmpl b/go/core/internal/controller/translator/agent/docker-auth-init.sh.tmpl new file mode 100644 index 000000000..505118d77 --- /dev/null +++ b/go/core/internal/controller/translator/agent/docker-auth-init.sh.tmpl @@ -0,0 +1,9 @@ +set -e +mkdir -p /docker-config-out +merged='{"auths":{}}' +{{- range .Secrets }} +if [ -f /docker-secrets/{{ . }}/.dockerconfigjson ]; then + merged="$(printf '%s\n%s\n' "$merged" "$(cat /docker-secrets/{{ . }}/.dockerconfigjson)" | jq -s '.[0].auths * .[1].auths | {"auths": .}')" +fi +{{- end }} +printf '%s' "$merged" > /docker-config-out/config.json diff --git a/go/core/internal/controller/translator/agent/git_skills_test.go b/go/core/internal/controller/translator/agent/git_skills_test.go index b5541140d..53b0d32ca 100644 --- a/go/core/internal/controller/translator/agent/git_skills_test.go +++ b/go/core/internal/controller/translator/agent/git_skills_test.go @@ -428,6 +428,224 @@ func Test_AdkApiTranslator_Skills(t *testing.T) { } } +func Test_AdkApiTranslator_SkillsImagePullSecrets(t *testing.T) { + scheme := schemev1.Scheme + require.NoError(t, v1alpha2.AddToScheme(scheme)) + + namespace := "default" + modelName := "test-model" + + modelConfig := &v1alpha2.ModelConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: modelName, + Namespace: namespace, + }, + Spec: v1alpha2.ModelConfigSpec{ + Model: "gpt-4", + Provider: v1alpha2.ModelProviderOpenAI, + }, + } + + defaultModel := types.NamespacedName{ + Namespace: namespace, + Name: modelName, + } + + tests := []struct { + name string + agent *v1alpha2.Agent + wantDockerAuthInit bool + wantInitCount int + }{ + { + name: "OCI skills without imagePullSecrets - single init container", + agent: &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "agent-no-pull-secret", Namespace: namespace}, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Declarative: &v1alpha2.DeclarativeAgentSpec{ + SystemMessage: "test", + ModelConfig: modelName, + }, + Skills: &v1alpha2.SkillForAgent{ + Refs: []string{"ghcr.io/org/skill:v1"}, + }, + }, + }, + wantDockerAuthInit: false, + wantInitCount: 1, + }, + { + name: "OCI skills with single imagePullSecret - two init containers", + agent: &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "agent-one-pull-secret", Namespace: namespace}, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Declarative: &v1alpha2.DeclarativeAgentSpec{ + SystemMessage: "test", + ModelConfig: modelName, + }, + Skills: &v1alpha2.SkillForAgent{ + Refs: []string{"docker.artifactory.example.com/org/skill:v1"}, + ImagePullSecrets: []corev1.LocalObjectReference{{Name: "registry-credentials"}}, + }, + }, + }, + wantDockerAuthInit: true, + wantInitCount: 2, + }, + { + name: "OCI skills with multiple imagePullSecrets - two init containers merging all auths", + agent: &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "agent-multi-pull-secrets", Namespace: namespace}, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Declarative: &v1alpha2.DeclarativeAgentSpec{ + SystemMessage: "test", + ModelConfig: modelName, + }, + Skills: &v1alpha2.SkillForAgent{ + Refs: []string{ + "docker.artifactory.example.com/org/skill-a:v1", + "acr.azurecr.io/org/skill-b:v2", + }, + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: "artifactory-creds"}, + {Name: "acr-creds"}, + }, + }, + }, + }, + wantDockerAuthInit: true, + wantInitCount: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + kubeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(modelConfig, tt.agent). + Build() + + trans := translator.NewAdkApiTranslator(kubeClient, defaultModel, nil, "", nil) + + outputs, err := translator.TranslateAgent(context.Background(), trans, tt.agent) + require.NoError(t, err) + require.NotNil(t, outputs) + + var deployment *appsv1.Deployment + for _, obj := range outputs.Manifest { + if d, ok := obj.(*appsv1.Deployment); ok { + deployment = d + } + } + require.NotNil(t, deployment, "Deployment should be created") + + initContainers := deployment.Spec.Template.Spec.InitContainers + assert.Len(t, initContainers, tt.wantInitCount, "unexpected number of init containers") + + // Find the skills-init container + var skillsInitContainer *corev1.Container + for i := range initContainers { + if initContainers[i].Name == "skills-init" { + skillsInitContainer = &initContainers[i] + } + } + require.NotNil(t, skillsInitContainer, "skills-init container should always exist") + + if tt.wantDockerAuthInit { + // Verify docker-auth-init container exists and is the first init container + require.True(t, len(initContainers) >= 2, "should have at least 2 init containers") + assert.Equal(t, "docker-auth-init", initContainers[0].Name, "docker-auth-init should be first") + + dockerAuthInit := &initContainers[0] + + // Verify merge script uses jq and writes to the correct path + require.Len(t, dockerAuthInit.Command, 3) + mergeScript := dockerAuthInit.Command[2] + assert.Contains(t, mergeScript, "jq") + assert.Contains(t, mergeScript, ".dockerconfigjson") + assert.Contains(t, mergeScript, "/docker-config-out/config.json") + + // Verify docker-config-out volume mount exists on docker-auth-init + hasConfigOut := false + for _, vm := range dockerAuthInit.VolumeMounts { + if vm.Name == "kagent-docker-config" && vm.MountPath == "/docker-config-out" { + hasConfigOut = true + } + } + assert.True(t, hasConfigOut, "docker-auth-init should mount kagent-docker-config at /docker-config-out") + + // Verify pull secret volumes and mounts are present on docker-auth-init + require.NotNil(t, tt.agent.Spec.Skills) + for _, ps := range tt.agent.Spec.Skills.ImagePullSecrets { + volName := "pull-secret-" + ps.Name + + // Volume on deployment + hasPullSecretVol := false + for _, v := range deployment.Spec.Template.Spec.Volumes { + if v.Name == volName && v.Secret != nil && v.Secret.SecretName == ps.Name { + hasPullSecretVol = true + } + } + assert.True(t, hasPullSecretVol, "pull-secret volume %q should exist", volName) + + // Mount on docker-auth-init + hasPullSecretMount := false + for _, vm := range dockerAuthInit.VolumeMounts { + if vm.Name == volName && vm.MountPath == "/docker-secrets/"+ps.Name && vm.ReadOnly { + hasPullSecretMount = true + } + } + assert.True(t, hasPullSecretMount, "docker-auth-init should mount pull-secret %q", volName) + + // Merge script references this secret + assert.Contains(t, mergeScript, "/docker-secrets/"+ps.Name+"/.dockerconfigjson") + } + + // Verify shared EmptyDir volume exists + hasDockerConfigVol := false + for _, v := range deployment.Spec.Template.Spec.Volumes { + if v.Name == "kagent-docker-config" { + hasDockerConfigVol = true + assert.NotNil(t, v.EmptyDir, "kagent-docker-config should be an EmptyDir volume") + } + } + assert.True(t, hasDockerConfigVol, "kagent-docker-config EmptyDir volume should exist") + + // Verify skills-init mounts the docker config and has DOCKER_CONFIG env + hasDockerMount := false + for _, vm := range skillsInitContainer.VolumeMounts { + if vm.Name == "kagent-docker-config" && vm.MountPath == "/.kagent/.docker" && vm.ReadOnly { + hasDockerMount = true + } + } + assert.True(t, hasDockerMount, "skills-init should mount kagent-docker-config at /.kagent/.docker") + + hasDockerConfigEnv := false + for _, e := range skillsInitContainer.Env { + if e.Name == "DOCKER_CONFIG" && e.Value == "/.kagent/.docker" { + hasDockerConfigEnv = true + } + } + assert.True(t, hasDockerConfigEnv, "skills-init should have DOCKER_CONFIG env var pointing to /.kagent/.docker") + } else { + // No imagePullSecrets: no docker-auth-init, no DOCKER_CONFIG env, no docker config volumes + for _, c := range initContainers { + assert.NotEqual(t, "docker-auth-init", c.Name, "docker-auth-init should not exist without imagePullSecrets") + } + for _, e := range skillsInitContainer.Env { + assert.NotEqual(t, "DOCKER_CONFIG", e.Name, "DOCKER_CONFIG env should not be set without imagePullSecrets") + } + for _, v := range deployment.Spec.Template.Spec.Volumes { + assert.NotEqual(t, "kagent-docker-config", v.Name, "kagent-docker-config volume should not exist") + } + } + }) + } +} + func Test_AdkApiTranslator_SkillsConfigurableImage(t *testing.T) { scheme := schemev1.Scheme require.NoError(t, v1alpha2.AddToScheme(scheme)) diff --git a/go/core/internal/controller/translator/agent/manifest_builder.go b/go/core/internal/controller/translator/agent/manifest_builder.go index 5a4e543fa..dcbaee9c4 100644 --- a/go/core/internal/controller/translator/agent/manifest_builder.go +++ b/go/core/internal/controller/translator/agent/manifest_builder.go @@ -386,13 +386,14 @@ func buildSkillsRuntime( manifestCtx.deployment.SecurityContext, initEnv, getDefaultResources(initResources), + spec.Skills.ImagePullSecrets, ) if err != nil { return nil, fmt.Errorf("failed to build skills init container: %w", err) } *volumes = append(*volumes, skillsVolumes...) - return []corev1.Container{container}, nil + return container, nil } func projectedTokenVolume() corev1.Volume { diff --git a/go/core/test/e2e/invoke_api_test.go b/go/core/test/e2e/invoke_api_test.go index b8de767d7..b463f1da9 100644 --- a/go/core/test/e2e/invoke_api_test.go +++ b/go/core/test/e2e/invoke_api_test.go @@ -13,6 +13,7 @@ import ( "testing" "time" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" k8s_runtime "k8s.io/apimachinery/pkg/runtime" @@ -83,6 +84,8 @@ func setupK8sClient(t *testing.T, includeV1Alpha1 bool) client.Client { } err = corev1.AddToScheme(scheme) require.NoError(t, err) + err = appsv1.AddToScheme(scheme) + require.NoError(t, err) cli, err := client.New(cfg, client.Options{ Scheme: scheme, @@ -1146,6 +1149,66 @@ func TestE2EInvokeSkillInAgent(t *testing.T) { runSyncTest(t, a2aClient, "make me a kebab", "Pick it up from around the corner", nil) } +func TestE2ESkillImagePullSecrets(t *testing.T) { + // Setup mock server + baseURL, stopServer := setupMockServer(t, "mocks/invoke_skill.json") + defer stopServer() + + // Setup Kubernetes client + cli := setupK8sClient(t, false) + + // Create a dummy dockerconfigjson secret. + // The kind-registry is unauthenticated, so credentials don't matter — + // we're testing that the controller wires up the docker-auth-init container. + dockerConfigJSON := `{"auths":{"kind-registry:5000":{"username":"user","password":"pass","auth":"dXNlcjpwYXNz"}}}` + pullSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-pull-secret-", + Namespace: "kagent", + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + corev1.DockerConfigJsonKey: []byte(dockerConfigJSON), + }, + } + require.NoError(t, cli.Create(t.Context(), pullSecret)) + cleanup(t, cli, pullSecret) + + // Setup model config and agent with imagePullSecrets + modelCfg := setupModelConfig(t, cli, baseURL) + agent := setupAgentWithOptions(t, cli, modelCfg.Name, nil, AgentOptions{ + Skills: &v1alpha2.SkillForAgent{ + InsecureSkipVerify: true, + Refs: []string{"kind-registry:5000/kebab-maker:latest"}, + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: pullSecret.Name}, + }, + }, + }) + + // Verify the Deployment has the docker-auth-init init container + deployment := &appsv1.Deployment{} + require.NoError(t, cli.Get(t.Context(), client.ObjectKey{Name: agent.Name, Namespace: agent.Namespace}, deployment)) + initContainers := deployment.Spec.Template.Spec.InitContainers + require.Len(t, initContainers, 2, "expected docker-auth-init + skills-init init containers") + require.Equal(t, "docker-auth-init", initContainers[0].Name) + require.Equal(t, "skills-init", initContainers[1].Name) + + // Verify docker-auth-init mounts all pull secrets + var foundSecretMount bool + for _, vol := range initContainers[0].VolumeMounts { + if strings.Contains(vol.Name, "pull-secret") { + foundSecretMount = true + break + } + } + require.True(t, foundSecretMount, "docker-auth-init should mount the pull secret volume") + + // Verify the agent works end-to-end with the skill + a2aClient := setupA2AClient(t, agent) + runSyncTest(t, a2aClient, "make me a kebab", "Pick it up from around the corner", nil) +} + func TestE2EDeclarativeAgentNetworkAllowlistWithSkills(t *testing.T) { runDeclarativeAgentNetworkAllowlistWithSkills(t, "python", nil) } diff --git a/helm/kagent-crds/templates/kagent.dev_agents.yaml b/helm/kagent-crds/templates/kagent.dev_agents.yaml index 1c95c6912..7e36bc1bd 100644 --- a/helm/kagent-crds/templates/kagent.dev_agents.yaml +++ b/helm/kagent-crds/templates/kagent.dev_agents.yaml @@ -13319,6 +13319,31 @@ spec: maxItems: 20 minItems: 1 type: array + imagePullSecrets: + description: |- + ImagePullSecrets is a list of references to secrets in the same namespace to use for + pulling skill images from private registries. Each referenced secret must be of type + kubernetes.io/dockerconfigjson. The credentials from all secrets are merged and made + available to the skills-init container at /.kagent/.docker/config.json; krane will + use them automatically when pulling images. + 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 + maxItems: 20 + type: array initContainer: description: Configuration for the skills-init init container. properties: diff --git a/helm/kagent-crds/templates/kagent.dev_sandboxagents.yaml b/helm/kagent-crds/templates/kagent.dev_sandboxagents.yaml index f790af205..5bf46cce1 100644 --- a/helm/kagent-crds/templates/kagent.dev_sandboxagents.yaml +++ b/helm/kagent-crds/templates/kagent.dev_sandboxagents.yaml @@ -10969,6 +10969,31 @@ spec: maxItems: 20 minItems: 1 type: array + imagePullSecrets: + description: |- + ImagePullSecrets is a list of references to secrets in the same namespace to use for + pulling skill images from private registries. Each referenced secret must be of type + kubernetes.io/dockerconfigjson. The credentials from all secrets are merged and made + available to the skills-init container at /.kagent/.docker/config.json; krane will + use them automatically when pulling images. + 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 + maxItems: 20 + type: array initContainer: description: Configuration for the skills-init init container. properties: diff --git a/python/pyproject.toml b/python/pyproject.toml index c12bdf0ae..2e041047d 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -6,7 +6,7 @@ dev = [ "pytest>=9.0.3", "pytest-asyncio>=0.25.3", "ruff>=0.15.12", - "authlib>=1.7.0" + "authlib>=1.7.1" ] [tool.uv] diff --git a/python/uv.lock b/python/uv.lock index e2a4a2042..360afbbbc 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -35,7 +35,7 @@ constraints = [ [manifest.dependency-groups] dev = [ - { name = "authlib", specifier = ">=1.7.0" }, + { name = "authlib", specifier = ">=1.7.1" }, { name = "pytest", specifier = ">=9.0.3" }, { name = "pytest-asyncio", specifier = ">=0.25.3" }, { name = "ruff", specifier = ">=0.15.12" }, @@ -379,15 +379,15 @@ wheels = [ [[package]] name = "authlib" -version = "1.7.0" +version = "1.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "joserfc" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d9/82/4d0603f30c1b4629b1f091bb266b0d7986434891d6940a8c87f8098db24e/authlib-1.7.0.tar.gz", hash = "sha256:b3e326c9aa9cc3ea95fe7d89fd880722d3608da4d00e8a27e061e64b48d801d5", size = 175890, upload-time = "2026-04-18T11:00:28.559Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/f2/e05664d5275ce811fd4e9df0a2b3f0086ee19a8a80358d95499fa82fd50c/authlib-1.7.1.tar.gz", hash = "sha256:8c09b0f9d080c823e594b52316af70f79a1fa4eed64d0363a076233c04ef063a", size = 175884, upload-time = "2026-05-04T08:11:25.033Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/48/c954218b2a250e23f178f10167c4173fecb5a75d2c206f0a67ba58006c26/authlib-1.7.0-py2.py3-none-any.whl", hash = "sha256:e36817afb02f6f0b6bf55f150782499ddd6ddf44b402bb055d3263cc65ac9ae0", size = 258779, upload-time = "2026-04-18T11:00:26.64Z" }, + { url = "https://files.pythonhosted.org/packages/e0/82/730650ee5e5b598b7bfdc291b784bc2f6fe02a5671695485403365101088/authlib-1.7.1-py2.py3-none-any.whl", hash = "sha256:8470f4aa6b5590ac41bd81d6e6ee12448ce36a0da0af19bbed69fb53fb4e8ad9", size = 258826, upload-time = "2026-05-04T08:11:23.208Z" }, ] [[package]]