From de936786f7fa7dcf6d9296ff27a8fd0699a384cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=9F=E5=AE=9D=E5=9D=A4?= Date: Thu, 11 Jun 2026 19:18:19 +0800 Subject: [PATCH 1/3] feat(sandbox): add environment variable injection to KubernetesFilesystemSpec KubernetesFilesystemSpec now supports environment(Map), mirroring the existing DockerFilesystemSpec API. Env vars are passed to the K8s Pod container spec via EnvVar entries in Fabric8 ContainerBuilder. - KubernetesSandboxClientOptions: add environment field with getter/setter - KubernetesFilesystemSpec: add fluent environment() builder method - KubernetesSandboxClient: include environment in merge() and copy() - Fabric8KubernetesPodRuntime: apply env map via cb.withEnv() in createPod() --- .../kubernetes/Fabric8KubernetesPodRuntime.java | 16 ++++++++++++++++ .../kubernetes/KubernetesFilesystemSpec.java | 5 +++++ .../kubernetes/KubernetesSandboxClient.java | 8 ++++++++ .../KubernetesSandboxClientOptions.java | 10 ++++++++++ 4 files changed, 39 insertions(+) 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..51da67fcf 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 != null && !env.isEmpty()) { + List envVars = + env.entrySet().stream() + .map( + e -> + new EnvVarBuilder() + .withName(e.getKey()) + .withValue(e.getValue()) + .build()) + .toList(); + cb.withEnv(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..e6baa7618 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; @@ -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<>(base.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<>(); + } } From 42a8cefb5df2db61728f9c5de5b822e176b45546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=9F=E5=AE=9D=E5=9D=A4?= Date: Thu, 11 Jun 2026 19:21:28 +0800 Subject: [PATCH 2/3] fix(sandbox): address code review findings on env injection - merge(): source from o instead of base for consistency with other fields - createPod(): drop redundant null-check, switch to addAllToEnv() for safety --- .../sandbox/kubernetes/Fabric8KubernetesPodRuntime.java | 4 ++-- .../sandbox/kubernetes/KubernetesSandboxClient.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 51da67fcf..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 @@ -212,7 +212,7 @@ private void createPod(KubernetesSandboxState state) { .withArgs("while true; do sleep 3600; done"); Map env = templateOptions.getEnvironment(); - if (env != null && !env.isEmpty()) { + if (!env.isEmpty()) { List envVars = env.entrySet().stream() .map( @@ -222,7 +222,7 @@ private void createPod(KubernetesSandboxState state) { .withValue(e.getValue()) .build()) .toList(); - cb.withEnv(envVars); + cb.addAllToEnv(envVars); } if (templateOptions.getCpuRequest() != null || templateOptions.getMemoryRequest() != null) { 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 e6baa7618..ed0a667ee 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 @@ -174,7 +174,7 @@ private KubernetesSandboxClientOptions merge(KubernetesSandboxClientOptions call o.setMemoryRequest(callOptions.getMemoryRequest()); } if (callOptions.getEnvironment() != null && !callOptions.getEnvironment().isEmpty()) { - Map merged = new HashMap<>(base.getEnvironment()); + Map merged = new HashMap<>(o.getEnvironment()); merged.putAll(callOptions.getEnvironment()); o.setEnvironment(merged); } From 49adc4af573e372c0587831c15e64ee629a54ba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=9F=E5=AE=9D=E5=9D=A4?= Date: Thu, 11 Jun 2026 19:28:09 +0800 Subject: [PATCH 3/3] test(sandbox): add unit tests for KubernetesFilesystemSpec env injection Cover KubernetesSandboxClientOptions defensive copy, null-safety, unmodifiable getter, and KubernetesSandboxClient.merge() semantics: - null call options preserves spec-level env - empty call-level env preserves spec-level env - call-level key overrides spec-level key - call-level key adds to spec-level env (non-overridden keys preserved) Also widen merge() visibility to package-private to enable testing. --- .../kubernetes/KubernetesSandboxClient.java | 2 +- .../KubernetesSandboxClientOptionsTest.java | 113 ++++++++++++++++++ 2 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 agentscope-extensions/agentscope-extensions-sandbox/agentscope-extensions-sandbox-kubernetes/src/test/java/io/agentscope/extensions/sandbox/kubernetes/KubernetesSandboxClientOptionsTest.java 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 ed0a667ee..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 @@ -133,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) { 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); + } +}