fix(dsh): isolate runtime child environment

This commit is contained in:
matevip 2026-08-26 05:13:55 -04:00
parent ab5a0a651b
commit fb94027038
2 changed files with 82 additions and 21 deletions

View File

@ -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", "<empty>"),
Map<String, String> environment = builder.environment();
Map<String, String> 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", "<empty>"),
!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<String, String> childEnvironment(Map<String, String> inherited,
RuntimeSession session,
DshRuntimeConfiguration configuration,
ModelProviderEntity provider) {
Map<String, String> 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<String, String> source, Map<String, String> target, String key) {
if (source == null) return;
putIfPresent(target, key, source.get(key));
}
private static void putIfPresent(Map<String, String> 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 {

View File

@ -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<String, String> 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<String, String> 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());
}
}