diff --git a/agentscope-extensions/agentscope-extensions-sandbox/agentscope-extensions-sandbox-kubernetes/src/main/java/io/agentscope/extensions/sandbox/kubernetes/Fabric8KubernetesPodRuntime.java b/agentscope-extensions/agentscope-extensions-sandbox/agentscope-extensions-sandbox-kubernetes/src/main/java/io/agentscope/extensions/sandbox/kubernetes/Fabric8KubernetesPodRuntime.java index 7fef79da8..7bf6d669f 100644 --- a/agentscope-extensions/agentscope-extensions-sandbox/agentscope-extensions-sandbox-kubernetes/src/main/java/io/agentscope/extensions/sandbox/kubernetes/Fabric8KubernetesPodRuntime.java +++ b/agentscope-extensions/agentscope-extensions-sandbox/agentscope-extensions-sandbox-kubernetes/src/main/java/io/agentscope/extensions/sandbox/kubernetes/Fabric8KubernetesPodRuntime.java @@ -22,6 +22,8 @@ import io.agentscope.harness.agent.sandbox.WorkspaceSpec; import io.agentscope.harness.agent.sandbox.layout.BindMountEntry; import io.fabric8.kubernetes.api.model.ContainerBuilder; +import io.fabric8.kubernetes.api.model.EnvVar; +import io.fabric8.kubernetes.api.model.EnvVarBuilder; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.PodBuilder; import io.fabric8.kubernetes.api.model.PodSpecBuilder; @@ -209,6 +211,20 @@ private void createPod(KubernetesSandboxState state) { .withCommand("sh", "-c") .withArgs("while true; do sleep 3600; done"); + Map env = templateOptions.getEnvironment(); + if (!env.isEmpty()) { + List envVars = + env.entrySet().stream() + .map( + e -> + new EnvVarBuilder() + .withName(e.getKey()) + .withValue(e.getValue()) + .build()) + .toList(); + cb.addAllToEnv(envVars); + } + if (templateOptions.getCpuRequest() != null || templateOptions.getMemoryRequest() != null) { ResourceRequirementsBuilder rb = new ResourceRequirementsBuilder(); Map requests = new HashMap<>(); diff --git a/agentscope-extensions/agentscope-extensions-sandbox/agentscope-extensions-sandbox-kubernetes/src/main/java/io/agentscope/extensions/sandbox/kubernetes/KubernetesFilesystemSpec.java b/agentscope-extensions/agentscope-extensions-sandbox/agentscope-extensions-sandbox-kubernetes/src/main/java/io/agentscope/extensions/sandbox/kubernetes/KubernetesFilesystemSpec.java index c03de884c..01ba1d42f 100644 --- a/agentscope-extensions/agentscope-extensions-sandbox/agentscope-extensions-sandbox-kubernetes/src/main/java/io/agentscope/extensions/sandbox/kubernetes/KubernetesFilesystemSpec.java +++ b/agentscope-extensions/agentscope-extensions-sandbox/agentscope-extensions-sandbox-kubernetes/src/main/java/io/agentscope/extensions/sandbox/kubernetes/KubernetesFilesystemSpec.java @@ -87,6 +87,11 @@ public KubernetesFilesystemSpec memoryRequest(String memoryRequest) { return this; } + public KubernetesFilesystemSpec environment(Map environment) { + options.setEnvironment(environment); + return this; + } + public KubernetesFilesystemSpec snapshotSpec(SandboxSnapshotSpec snapshotSpec) { this.snapshotSpec = snapshotSpec; return this; diff --git a/agentscope-extensions/agentscope-extensions-sandbox/agentscope-extensions-sandbox-kubernetes/src/main/java/io/agentscope/extensions/sandbox/kubernetes/KubernetesSandboxClient.java b/agentscope-extensions/agentscope-extensions-sandbox/agentscope-extensions-sandbox-kubernetes/src/main/java/io/agentscope/extensions/sandbox/kubernetes/KubernetesSandboxClient.java index 10974bcb9..6cf5d657e 100644 --- a/agentscope-extensions/agentscope-extensions-sandbox/agentscope-extensions-sandbox-kubernetes/src/main/java/io/agentscope/extensions/sandbox/kubernetes/KubernetesSandboxClient.java +++ b/agentscope-extensions/agentscope-extensions-sandbox/agentscope-extensions-sandbox-kubernetes/src/main/java/io/agentscope/extensions/sandbox/kubernetes/KubernetesSandboxClient.java @@ -25,6 +25,8 @@ import io.agentscope.harness.agent.sandbox.snapshot.SandboxSnapshotSpec; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -131,7 +133,7 @@ public SandboxState deserializeState(String json) { } } - private KubernetesSandboxClientOptions merge(KubernetesSandboxClientOptions callOptions) { + KubernetesSandboxClientOptions merge(KubernetesSandboxClientOptions callOptions) { KubernetesSandboxClientOptions base = defaultOptions != null ? defaultOptions : new KubernetesSandboxClientOptions(); if (callOptions == null) { @@ -171,6 +173,11 @@ private KubernetesSandboxClientOptions merge(KubernetesSandboxClientOptions call if (callOptions.getMemoryRequest() != null) { o.setMemoryRequest(callOptions.getMemoryRequest()); } + if (callOptions.getEnvironment() != null && !callOptions.getEnvironment().isEmpty()) { + Map merged = new HashMap<>(o.getEnvironment()); + merged.putAll(callOptions.getEnvironment()); + o.setEnvironment(merged); + } return o; } @@ -187,6 +194,7 @@ private static KubernetesSandboxClientOptions copy(KubernetesSandboxClientOption o.setPodLabels(src.getPodLabels()); o.setCpuRequest(src.getCpuRequest()); o.setMemoryRequest(src.getMemoryRequest()); + o.setEnvironment(src.getEnvironment()); return o; } diff --git a/agentscope-extensions/agentscope-extensions-sandbox/agentscope-extensions-sandbox-kubernetes/src/main/java/io/agentscope/extensions/sandbox/kubernetes/KubernetesSandboxClientOptions.java b/agentscope-extensions/agentscope-extensions-sandbox/agentscope-extensions-sandbox-kubernetes/src/main/java/io/agentscope/extensions/sandbox/kubernetes/KubernetesSandboxClientOptions.java index 1b4e7740b..49f8042a0 100644 --- a/agentscope-extensions/agentscope-extensions-sandbox/agentscope-extensions-sandbox-kubernetes/src/main/java/io/agentscope/extensions/sandbox/kubernetes/KubernetesSandboxClientOptions.java +++ b/agentscope-extensions/agentscope-extensions-sandbox/agentscope-extensions-sandbox-kubernetes/src/main/java/io/agentscope/extensions/sandbox/kubernetes/KubernetesSandboxClientOptions.java @@ -19,6 +19,7 @@ import io.agentscope.harness.agent.sandbox.SandboxClientOptions; import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.KubernetesClient; +import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -36,6 +37,7 @@ public class KubernetesSandboxClientOptions extends SandboxClientOptions { private Map podLabels = new HashMap<>(); private String cpuRequest; private String memoryRequest; + private Map environment = new HashMap<>(); @Override public String getType() { @@ -138,4 +140,12 @@ public String getMemoryRequest() { public void setMemoryRequest(String memoryRequest) { this.memoryRequest = memoryRequest; } + + public Map getEnvironment() { + return Collections.unmodifiableMap(environment); + } + + public void setEnvironment(Map environment) { + this.environment = environment != null ? new HashMap<>(environment) : new HashMap<>(); + } } diff --git a/agentscope-extensions/agentscope-extensions-sandbox/agentscope-extensions-sandbox-kubernetes/src/test/java/io/agentscope/extensions/sandbox/kubernetes/KubernetesSandboxClientOptionsTest.java b/agentscope-extensions/agentscope-extensions-sandbox/agentscope-extensions-sandbox-kubernetes/src/test/java/io/agentscope/extensions/sandbox/kubernetes/KubernetesSandboxClientOptionsTest.java new file mode 100644 index 000000000..c23019caa --- /dev/null +++ b/agentscope-extensions/agentscope-extensions-sandbox/agentscope-extensions-sandbox-kubernetes/src/test/java/io/agentscope/extensions/sandbox/kubernetes/KubernetesSandboxClientOptionsTest.java @@ -0,0 +1,113 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.agentscope.extensions.sandbox.kubernetes; + +import java.util.Map; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class KubernetesSandboxClientOptionsTest { + + // -- KubernetesSandboxClientOptions -- + + @Test + void environmentDefaultsToEmpty() { + KubernetesSandboxClientOptions opts = new KubernetesSandboxClientOptions(); + Assertions.assertNotNull(opts.getEnvironment()); + Assertions.assertTrue(opts.getEnvironment().isEmpty()); + } + + @Test + void setEnvironmentDefensivelyCopies() { + KubernetesSandboxClientOptions opts = new KubernetesSandboxClientOptions(); + Map original = new java.util.HashMap<>(); + original.put("K", "V"); + opts.setEnvironment(original); + original.put("K2", "V2"); + Assertions.assertFalse( + opts.getEnvironment().containsKey("K2"), + "setEnvironment should copy, not retain original reference"); + } + + @Test + void setEnvironmentNullClearsMap() { + KubernetesSandboxClientOptions opts = new KubernetesSandboxClientOptions(); + opts.setEnvironment(Map.of("K", "V")); + opts.setEnvironment(null); + Assertions.assertTrue(opts.getEnvironment().isEmpty()); + } + + @Test + void getEnvironmentIsUnmodifiable() { + KubernetesSandboxClientOptions opts = new KubernetesSandboxClientOptions(); + opts.setEnvironment(Map.of("K", "V")); + Assertions.assertThrows( + UnsupportedOperationException.class, () -> opts.getEnvironment().put("X", "Y")); + } + + // -- KubernetesSandboxClient merge / copy -- + + @Test + void mergeNoCallOptionsPreservesSpecEnv() { + KubernetesSandboxClient client = clientWithSpecEnv(Map.of("SPEC_KEY", "spec_val")); + KubernetesSandboxClientOptions merged = client.merge(null); + Assertions.assertEquals("spec_val", merged.getEnvironment().get("SPEC_KEY")); + } + + @Test + void mergeEmptyCallEnvPreservesSpecEnv() { + KubernetesSandboxClient client = clientWithSpecEnv(Map.of("SPEC_KEY", "spec_val")); + KubernetesSandboxClientOptions call = new KubernetesSandboxClientOptions(); + KubernetesSandboxClientOptions merged = client.merge(call); + Assertions.assertEquals("spec_val", merged.getEnvironment().get("SPEC_KEY")); + } + + @Test + void mergeCallEnvOverridesSpecEnv() { + KubernetesSandboxClient client = clientWithSpecEnv(Map.of("KEY", "spec_val")); + KubernetesSandboxClientOptions call = new KubernetesSandboxClientOptions(); + call.setEnvironment(Map.of("KEY", "call_val")); + KubernetesSandboxClientOptions merged = client.merge(call); + Assertions.assertEquals( + "call_val", + merged.getEnvironment().get("KEY"), + "call-level env should override spec-level env for same key"); + } + + @Test + void mergeCallEnvAddsToSpecEnv() { + KubernetesSandboxClient client = clientWithSpecEnv(Map.of("SPEC_KEY", "spec_val")); + KubernetesSandboxClientOptions call = new KubernetesSandboxClientOptions(); + call.setEnvironment(Map.of("CALL_KEY", "call_val")); + KubernetesSandboxClientOptions merged = client.merge(call); + Assertions.assertEquals( + "spec_val", + merged.getEnvironment().get("SPEC_KEY"), + "spec-level keys absent from call-level should be preserved"); + Assertions.assertEquals( + "call_val", + merged.getEnvironment().get("CALL_KEY"), + "call-level keys should be added"); + } + + // -- helpers -- + + private static KubernetesSandboxClient clientWithSpecEnv(Map env) { + KubernetesSandboxClientOptions spec = new KubernetesSandboxClientOptions(); + spec.setEnvironment(env); + return new KubernetesSandboxClient(spec); + } +}