From a2cc65886ee539d90a9d8a2adbcdd9b6bbe1ec12 Mon Sep 17 00:00:00 2001 From: u010143241 Date: Sun, 12 Jul 2026 09:00:30 +0800 Subject: [PATCH] fix(harness): stream docker exec commands through stdin --- .../sandbox/impl/docker/DockerSandbox.java | 31 ++++--- .../impl/docker/DockerSandboxCommandTest.java | 89 +++++++++++++++++++ 2 files changed, 108 insertions(+), 12 deletions(-) create mode 100644 agentscope-harness/src/test/java/io/agentscope/harness/agent/sandbox/impl/docker/DockerSandboxCommandTest.java diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/sandbox/impl/docker/DockerSandbox.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/sandbox/impl/docker/DockerSandbox.java index 30fe840429..c717bf0463 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/sandbox/impl/docker/DockerSandbox.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/sandbox/impl/docker/DockerSandbox.java @@ -57,7 +57,8 @@ * *

Workspace Operations

* @@ -138,17 +139,7 @@ protected ExecResult doExec(RuntimeContext runtimeContext, String command, int t String containerId = dockerState.getContainerId(); String workspaceRoot = dockerState.getWorkspaceRoot(); - List cmd = new ArrayList<>(); - cmd.add("docker"); - cmd.add("exec"); - cmd.add("-w"); - cmd.add(workspaceRoot); - cmd.add(containerId); - cmd.add("sh"); - cmd.add("-c"); - cmd.add(command); - - ProcessBuilder pb = new ProcessBuilder(cmd); + ProcessBuilder pb = new ProcessBuilder(buildExecCommand(containerId, workspaceRoot)); Process process = pb.start(); ExecutorService drainer = @@ -169,6 +160,14 @@ protected ExecResult doExec(RuntimeContext runtimeContext, String command, int t drainer.submit(() -> readStream(process.getErrorStream(), OUTPUT_TRUNCATE_BYTES)); drainer.shutdown(); + try (OutputStream stdin = process.getOutputStream()) { + writeShellProgram(stdin, command); + } catch (IOException e) { + process.destroyForcibly(); + drainer.shutdownNow(); + throw e; + } + boolean exited = process.waitFor(timeoutSeconds, TimeUnit.SECONDS); if (!exited) { process.destroyForcibly(); @@ -190,6 +189,14 @@ protected ExecResult doExec(RuntimeContext runtimeContext, String command, int t return result; } + static List buildExecCommand(String containerId, String workspaceRoot) { + return List.of("docker", "exec", "-i", "-w", workspaceRoot, containerId, "sh", "-s"); + } + + static void writeShellProgram(OutputStream stdin, String command) throws IOException { + stdin.write(command.getBytes(StandardCharsets.UTF_8)); + } + @Override protected InputStream doPersistWorkspace() throws Exception { String containerId = dockerState.getContainerId(); diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/sandbox/impl/docker/DockerSandboxCommandTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/sandbox/impl/docker/DockerSandboxCommandTest.java new file mode 100644 index 0000000000..ca4b58dee7 --- /dev/null +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/sandbox/impl/docker/DockerSandboxCommandTest.java @@ -0,0 +1,89 @@ +/* + * 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.harness.agent.sandbox.impl.docker; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import io.agentscope.core.agent.RuntimeContext; +import io.agentscope.harness.agent.sandbox.ExecResult; +import io.agentscope.harness.agent.sandbox.Sandbox; +import io.agentscope.harness.agent.sandbox.WorkspaceSpec; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; + +class DockerSandboxCommandTest { + + @Test + void execUsesStdinInsteadOfEmbeddingComplexShellProgramInHostArguments() throws Exception { + String script = + "mkdir -p \"$(dirname '/workspace/a b/result.txt')\" && " + + "printf '%s' \"nested 'quotes' and $HOME\" > '/workspace/a b/result.txt'"; + + List command = DockerSandbox.buildExecCommand("container-id", "/workspace"); + ByteArrayOutputStream stdin = new ByteArrayOutputStream(); + DockerSandbox.writeShellProgram(stdin, script); + + assertEquals( + List.of("docker", "exec", "-i", "-w", "/workspace", "container-id", "sh", "-s"), + command); + assertFalse(command.contains(script)); + assertEquals(script, stdin.toString(StandardCharsets.UTF_8)); + } + + @Test + void execHostArgumentsStayBoundedForLargeShellPrograms() throws Exception { + String script = "printf 'x%.0s' $(seq 1 100000)\n#" + "x".repeat(100_000); + + List command = DockerSandbox.buildExecCommand("container-id", "/workspace"); + ByteArrayOutputStream stdin = new ByteArrayOutputStream(); + DockerSandbox.writeShellProgram(stdin, script); + + assertEquals(8, command.size()); + assertFalse(command.stream().anyMatch(argument -> argument.length() > 100)); + assertEquals(script.getBytes(StandardCharsets.UTF_8).length, stdin.size()); + } + + @Test + @EnabledIfEnvironmentVariable(named = "AGENTSCOPE_DOCKER_INTEGRATION", matches = "true") + void realDockerExecPreservesComplexAndLargeShellPrograms() throws Exception { + DockerSandboxClient client = new DockerSandboxClient(); + DockerSandboxClientOptions options = + new DockerSandboxClientOptions().image("ubuntu:24.04").workspaceRoot("/workspace"); + try (Sandbox sandbox = client.create(new WorkspaceSpec(), null, options)) { + sandbox.start(); + + String complex = + "mkdir -p \"$(dirname '/workspace/a b/result.txt')\" && printf '%s' 'nested" + + " \"quotes\" and $HOME' > '/workspace/a b/result.txt' && cat" + + " '/workspace/a b/result.txt'"; + ExecResult complexResult = sandbox.exec(RuntimeContext.empty(), complex, 30); + assertEquals("nested \"quotes\" and $HOME", complexResult.stdout()); + + String large = + "#" + + "x".repeat(100_000) + + "\n" + + "printf '%s' 'large-script-ok' > /workspace/large.txt && cat" + + " /workspace/large.txt"; + ExecResult largeResult = sandbox.exec(RuntimeContext.empty(), large, 30); + assertEquals("large-script-ok", largeResult.stdout()); + } + } +}