diff --git a/controller/proposal/podspec_builder.go b/controller/proposal/podspec_builder.go index 1bc3b105..ba65f1ee 100644 --- a/controller/proposal/podspec_builder.go +++ b/controller/proposal/podspec_builder.go @@ -61,6 +61,15 @@ func (b *PodSpecBuilder) Build( var volumes []corev1.Volume + volumes = append(volumes, corev1.Volume{ + Name: "home", + VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, + }) + container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ + Name: "home", + MountPath: "/home/agent", + }) + container.Env = append(container.Env, corev1.EnvVar{Name: "LIGHTSPEED_PROVIDER", Value: providerTypeString(llm.Spec.Type)}, corev1.EnvVar{Name: "LIGHTSPEED_MODEL", Value: agent.Spec.Model}, @@ -205,8 +214,15 @@ func (b *PodSpecBuilder) buildSkills(skills []agenticv1alpha1.SkillsSource) ([]c }, }, } + workdirVol := corev1.Volume{ + Name: "skills-workdir", + VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, + } - var mounts []corev1.VolumeMount + mounts := []corev1.VolumeMount{{ + Name: "skills-workdir", + MountPath: "/app/skills/.agents", + }} if len(s.Paths) > 0 { baseMountPath := "/app/skills" for _, p := range s.Paths { @@ -221,7 +237,7 @@ func (b *PodSpecBuilder) buildSkills(skills []agenticv1alpha1.SkillsSource) ([]c } } - return []corev1.Volume{vol}, mounts + return []corev1.Volume{vol, workdirVol}, mounts } func (b *PodSpecBuilder) buildMCPServers(servers []agenticv1alpha1.MCPServerConfig) ([]corev1.Volume, []corev1.VolumeMount, []corev1.EnvVar, error) { diff --git a/controller/proposal/podspec_builder_test.go b/controller/proposal/podspec_builder_test.go index 3959f38a..4187c45e 100644 --- a/controller/proposal/podspec_builder_test.go +++ b/controller/proposal/podspec_builder_test.go @@ -100,6 +100,18 @@ func TestPodSpecBuilder_Anthropic(t *testing.T) { t.Errorf("missing credential volume mount at %s", llmCredsMountPath) } + // Verify home volume mount + foundHomeMount := false + for _, m := range container.VolumeMounts { + if m.Name == "home" && m.MountPath == "/home/agent" { + foundHomeMount = true + break + } + } + if !foundHomeMount { + t.Error("missing home volume mount at /home/agent") + } + // Verify volume foundVolume := false for _, v := range podSpec.Volumes { @@ -118,6 +130,21 @@ func TestPodSpecBuilder_Anthropic(t *testing.T) { t.Error("missing llm-credentials volume") } + // Verify home volume + foundHomeVolume := false + for _, v := range podSpec.Volumes { + if v.Name == "home" { + foundHomeVolume = true + if v.EmptyDir == nil { + t.Fatal("home volume should be emptyDir") + } + break + } + } + if !foundHomeVolume { + t.Error("missing home emptyDir volume") + } + // Verify readiness probe if container.ReadinessProbe == nil { t.Fatal("readinessProbe is nil") @@ -327,6 +354,7 @@ func TestPodSpecBuilder_Skills(t *testing.T) { } foundVolume := false + foundWorkdirVolume := false for _, v := range podSpec.Volumes { if v.Name == "skills" { foundVolume = true @@ -339,12 +367,34 @@ func TestPodSpecBuilder_Skills(t *testing.T) { if v.Image.PullPolicy != corev1.PullAlways { t.Errorf("skills pullPolicy = %v, want PullAlways", v.Image.PullPolicy) } - break + } + if v.Name == "skills-workdir" { + foundWorkdirVolume = true + if v.EmptyDir == nil { + t.Fatal("skills-workdir volume should be emptyDir") + } } } if !foundVolume { t.Error("missing skills volume") } + if !foundWorkdirVolume { + t.Error("missing skills-workdir volume") + } + + container := podSpec.Containers[0] + foundWorkdirMount := false + for _, m := range container.VolumeMounts { + if m.Name == "skills-workdir" && m.MountPath == "/app/skills/.agents" { + foundWorkdirMount = true + if m.ReadOnly { + t.Error("skills-workdir mount should be writable") + } + } + } + if !foundWorkdirMount { + t.Error("missing skills-workdir mount at /app/skills/.agents") + } } func TestPodSpecBuilder_SkillsWithPaths(t *testing.T) { @@ -372,6 +422,7 @@ func TestPodSpecBuilder_SkillsWithPaths(t *testing.T) { foundSearch := false foundAnalyze := false + foundWorkdirMount := false for _, m := range container.VolumeMounts { if m.Name == "skills" { if m.MountPath == "/app/skills/search.md" && m.SubPath == "skills/search.md" { @@ -384,6 +435,12 @@ func TestPodSpecBuilder_SkillsWithPaths(t *testing.T) { t.Error("skills mount should be readOnly") } } + if m.Name == "skills-workdir" && m.MountPath == "/app/skills/.agents" { + foundWorkdirMount = true + if m.ReadOnly { + t.Error("skills-workdir mount should be writable") + } + } } if !foundSearch { t.Error("missing search.md skill mount") @@ -391,6 +448,22 @@ func TestPodSpecBuilder_SkillsWithPaths(t *testing.T) { if !foundAnalyze { t.Error("missing analyze.md skill mount") } + if !foundWorkdirMount { + t.Error("missing skills-workdir mount at /app/skills/.agents") + } + + foundWorkdirVolume := false + for _, v := range podSpec.Volumes { + if v.Name == "skills-workdir" { + foundWorkdirVolume = true + if v.EmptyDir == nil { + t.Fatal("skills-workdir volume should be emptyDir") + } + } + } + if !foundWorkdirVolume { + t.Error("missing skills-workdir volume") + } } func TestPodSpecBuilder_RequiredSecrets_EnvVar(t *testing.T) { diff --git a/controller/sandbox/bootstrap.go b/controller/sandbox/bootstrap.go index 35b64746..30f3f0e2 100644 --- a/controller/sandbox/bootstrap.go +++ b/controller/sandbox/bootstrap.go @@ -130,6 +130,16 @@ func ensureSandboxTemplate(ctx context.Context, c client.Client, image, namespac "type": "RuntimeDefault", }, }, + "volumeMounts": []any{ + map[string]any{ + "name": "home", + "mountPath": "/home/agent", + }, + map[string]any{ + "name": "skills-workdir", + "mountPath": "/app/skills/.agents", + }, + }, }, }, "volumes": []any{ @@ -139,6 +149,14 @@ func ensureSandboxTemplate(ctx context.Context, c client.Client, image, namespac "reference": "placeholder:latest", }, }, + map[string]any{ + "name": "home", + "emptyDir": map[string]any{}, + }, + map[string]any{ + "name": "skills-workdir", + "emptyDir": map[string]any{}, + }, }, }, }, diff --git a/controller/sandbox/bootstrap_test.go b/controller/sandbox/bootstrap_test.go index 66da24af..284fcc0a 100644 --- a/controller/sandbox/bootstrap_test.go +++ b/controller/sandbox/bootstrap_test.go @@ -71,15 +71,54 @@ func TestEnsureBootstrapResources_CreatesResources(t *testing.T) { volumes, _, _ := unstructured.NestedSlice(tmpl.Object, "spec", "podTemplate", "spec", "volumes") foundSkills := false + foundHomeVolume := false + foundWorkdirVolume := false for _, v := range volumes { vol := v.(map[string]any) if vol["name"] == "skills" { foundSkills = true } + if vol["name"] == "home" { + foundHomeVolume = true + if vol["emptyDir"] == nil { + t.Error("home volume should be emptyDir") + } + } + if vol["name"] == "skills-workdir" { + foundWorkdirVolume = true + if vol["emptyDir"] == nil { + t.Error("skills-workdir volume should be emptyDir") + } + } } if !foundSkills { t.Error("SandboxTemplate missing skills volume") } + if !foundHomeVolume { + t.Error("SandboxTemplate missing home volume") + } + if !foundWorkdirVolume { + t.Error("SandboxTemplate missing skills-workdir volume") + } + + mounts, _, _ := unstructured.NestedSlice(container, "volumeMounts") + foundHomeMount := false + foundWorkdirMount := false + for _, m := range mounts { + mount := m.(map[string]any) + if mount["name"] == "home" && mount["mountPath"] == "/home/agent" { + foundHomeMount = true + } + if mount["name"] == "skills-workdir" && mount["mountPath"] == "/app/skills/.agents" { + foundWorkdirMount = true + } + } + if !foundHomeMount { + t.Error("SandboxTemplate missing home mount at /home/agent") + } + if !foundWorkdirMount { + t.Error("SandboxTemplate missing skills-workdir mount at /app/skills/.agents") + } lbls := tmpl.GetLabels() if lbls["app.kubernetes.io/managed-by"] != "lightspeed-operator" { diff --git a/test/agent/sandboxtemplate/sandboxtemplate.yaml b/test/agent/sandboxtemplate/sandboxtemplate.yaml index 3cc7d050..984ac011 100644 --- a/test/agent/sandboxtemplate/sandboxtemplate.yaml +++ b/test/agent/sandboxtemplate/sandboxtemplate.yaml @@ -39,7 +39,16 @@ spec: readOnlyRootFilesystem: true seccompProfile: type: RuntimeDefault + volumeMounts: + - name: home + mountPath: /home/agent + - name: skills-workdir + mountPath: /app/skills/.agents volumes: - name: skills image: reference: placeholder:latest + - name: home + emptyDir: {} + - name: skills-workdir + emptyDir: {}