diff --git a/mateclaw-server/src/main/java/vip/mate/agent/runtime/dsh/DshRuntimeService.java b/mateclaw-server/src/main/java/vip/mate/agent/runtime/dsh/DshRuntimeService.java index 5c3c8f80..608ac54b 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/runtime/dsh/DshRuntimeService.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/runtime/dsh/DshRuntimeService.java @@ -33,6 +33,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Files; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.UUID; @@ -256,28 +257,14 @@ public class DshRuntimeService implements AgentRuntimeProvider { ProcessBuilder builder = new ProcessBuilder(command) .directory(session.workingDirectory().toFile()) .redirectError(ProcessBuilder.Redirect.PIPE); - builder.environment().put("DSH_CWD", session.workingDirectory().toString()); - // The packaged binary gives the environment variable precedence - // over argv. Set the resolved path explicitly so IDEA/.env - // inheritance cannot select a different composition. - if (!configuration.cordisConfigPath().isBlank()) { - builder.environment().put("DSH_CORDIS_CONFIG", configuration.cordisConfigPath()); - } else { - builder.environment().remove("DSH_CORDIS_CONFIG"); - } - log.debug("[DSH] child environment: cordisConfig={}, exists={}", - builder.environment().getOrDefault("DSH_CORDIS_CONFIG", ""), + Map environment = builder.environment(); + Map childEnvironment = childEnvironment(environment, session, configuration, provider); + environment.clear(); + environment.putAll(childEnvironment); + log.debug("[DSH] child environment: keys={}, cordisConfig={}, exists={}", + environment.keySet(), + environment.getOrDefault("DSH_CORDIS_CONFIG", ""), !configuration.cordisConfigPath().isBlank() && Files.isRegularFile(Path.of(configuration.cordisConfigPath()))); - String apiKey = configuration.apiKey(); - if ((apiKey == null || apiKey.isBlank()) && provider != null) apiKey = provider.getApiKey(); - if (apiKey != null && !apiKey.isBlank()) { - builder.environment().put("DEEPSEEK_API_KEY", apiKey); - } - String baseUrl = configuration.baseUrl(); - if ((baseUrl == null || baseUrl.isBlank()) && provider != null) baseUrl = provider.getBaseUrl(); - if (baseUrl != null && !baseUrl.isBlank()) { - builder.environment().put("DEEPSEEK_BASE_URL", baseUrl); - } process = builder.start(); processRef.set(process); if (sink.isCancelled()) { @@ -463,6 +450,43 @@ public class DshRuntimeService implements AgentRuntimeProvider { return result; } + static Map childEnvironment(Map inherited, + RuntimeSession session, + DshRuntimeConfiguration configuration, + ModelProviderEntity provider) { + Map environment = new LinkedHashMap<>(); + copyIfPresent(inherited, environment, "PATH"); + copyIfPresent(inherited, environment, "HOME"); + copyIfPresent(inherited, environment, "USERPROFILE"); + copyIfPresent(inherited, environment, "TMPDIR"); + copyIfPresent(inherited, environment, "TEMP"); + copyIfPresent(inherited, environment, "TMP"); + copyIfPresent(inherited, environment, "SystemRoot"); + copyIfPresent(inherited, environment, "WINDIR"); + + environment.put("DSH_CWD", session.workingDirectory().toString()); + putIfPresent(environment, "DSH_CORDIS_CONFIG", configuration.cordisConfigPath()); + putIfPresent(environment, "DEEPSEEK_API_KEY", + firstNonBlank(configuration.apiKey(), provider == null ? null : provider.getApiKey())); + putIfPresent(environment, "DEEPSEEK_BASE_URL", + firstNonBlank(configuration.baseUrl(), provider == null ? null : provider.getBaseUrl())); + return environment; + } + + private static void copyIfPresent(Map source, Map target, String key) { + if (source == null) return; + putIfPresent(target, key, source.get(key)); + } + + private static void putIfPresent(Map target, String key, String value) { + if (value == null || value.isBlank()) return; + target.put(key, value); + } + + private static String firstNonBlank(String primary, String fallback) { + return primary != null && !primary.isBlank() ? primary : fallback; + } + private ModelProviderEntity resolveProvider(String modelName) { ModelConfigEntity model = null; try { diff --git a/mateclaw-server/src/test/java/vip/mate/agent/runtime/dsh/DshRuntimeServiceTest.java b/mateclaw-server/src/test/java/vip/mate/agent/runtime/dsh/DshRuntimeServiceTest.java index 59247abe..af78c4ea 100644 --- a/mateclaw-server/src/test/java/vip/mate/agent/runtime/dsh/DshRuntimeServiceTest.java +++ b/mateclaw-server/src/test/java/vip/mate/agent/runtime/dsh/DshRuntimeServiceTest.java @@ -10,10 +10,12 @@ import vip.mate.agent.runtime.contract.RuntimeEventType; import vip.mate.agent.runtime.contract.RuntimeSession; import vip.mate.agent.runtime.dsh.management.DshRuntimeConfigService; import vip.mate.agent.runtime.dsh.management.DshRuntimeConfiguration; +import vip.mate.llm.model.ModelProviderEntity; import vip.mate.llm.service.ModelConfigService; import vip.mate.llm.service.ModelProviderService; import java.nio.file.Path; +import java.util.HashMap; import java.util.Map; import java.util.List; import java.util.concurrent.TimeUnit; @@ -99,6 +101,36 @@ class DshRuntimeServiceTest { DshRuntimeService.commandLine("\"/opt/Deep Seek/dsh-jsonrpc-agent\" --stdio")); } + @Test + void childEnvironmentKeepsOnlyRuntimeVariablesAndExplicitCredentials() { + RuntimeSession session = session(Path.of("/workspace/project")); + DshRuntimeConfiguration configuration = new DshRuntimeConfiguration( + "/bin/dsh", "/opt/dsh/cordis.yml", "/workspace/global", + "https://configured.example/v1", "model", "configured-key"); + ModelProviderEntity provider = new ModelProviderEntity(); + provider.setApiKey("provider-key"); + provider.setBaseUrl("https://provider.example/v1"); + Map inherited = new HashMap<>(); + inherited.put("PATH", "/usr/bin"); + inherited.put("HOME", "/Users/mate"); + inherited.put("AWS_SECRET_ACCESS_KEY", "must-not-leak"); + inherited.put("DEEPSEEK_API_KEY", "inherited-key"); + inherited.put("DSH_CORDIS_CONFIG", "/stale/cordis.yml"); + + Map environment = DshRuntimeService.childEnvironment( + inherited, session, configuration, provider); + + assertEquals("/usr/bin", environment.get("PATH")); + assertEquals("/Users/mate", environment.get("HOME")); + assertEquals("/workspace/project", environment.get("DSH_CWD")); + assertEquals("/opt/dsh/cordis.yml", environment.get("DSH_CORDIS_CONFIG")); + assertEquals("configured-key", environment.get("DEEPSEEK_API_KEY")); + assertEquals("https://configured.example/v1", environment.get("DEEPSEEK_BASE_URL")); + assertFalse(environment.containsKey("AWS_SECRET_ACCESS_KEY")); + assertFalse(environment.containsValue("inherited-key")); + assertFalse(environment.containsValue("/stale/cordis.yml")); + } + private static DshRuntimeService service() { return service("/bin/dsh"); } @@ -111,4 +143,9 @@ class DshRuntimeServiceTest { Mockito.mock(ModelConfigService.class), Mockito.mock(ModelProviderService.class), config); } + + private static RuntimeSession session(Path workingDirectory) { + return new RuntimeSession("session-1", "conversation-1", 1L, 2L, + "model", workingDirectory, Map.of()); + } }