Environment
| 项 |
值 |
| agentscope-java |
2.0.0-RC5 |
| 模块 |
agentscope-extensions-sandbox-e2b |
| 受影响类 |
E2bEnvdProcessClient |
| JDK |
17 |
| 触发条件 |
E2bSandboxClientOptions.setCodec(E2bCodec.JSON) |
Summary
When E2bCodec.JSON is selected, any command that exits successfully (exit code 0) throws:
io.agentscope.harness.agent.sandbox.SandboxException$ExecException:
Command exited with code -2147483648:
-2147483648 is Integer.MIN_VALUE, the initial value of the exit variable in drainStartStream() — meaning no ProcessEvent.end frame was ever parsed.
Counter-intuitively, commands that fail (non-zero exit code) work correctly.
Root cause
proto3 semantics: for scalar fields (int32, bool, string...), when the value equals the default (0 for int32), the field is treated as unset:
message.getAllFields() does not include it
message.hasField(field) returns false
parseJsonStartResponse() uses getAllFields().isEmpty() to decide whether a message has content:
// E2bEnvdProcessClient.parseJsonStartResponse()
if (exitCodeNode.canConvertToInt()) {
endBuilder.setField(exitCodeField, exitCodeNode.intValue()); // sets exit_code = 0
}
if (!endBuilder.getAllFields().isEmpty()) { // ← false when exit_code == 0
event.setField(processEventDesc.findFieldByName("end"), endBuilder.build());
} // → end event silently dropped
if (!event.getAllFields().isEmpty()) { // ← also false, cascading
response.setField(startResponseDesc.findFieldByName("event"), event.build());
} // → event dropped too
Then in drainStartStream():
DynamicMessage sr = parseStartResponseFrame(data);
if (!sr.hasField(srEventF)) {
continue; // ← frame skipped entirely
}
And a second instance of the same class of bug:
Descriptors.FieldDescriptor ec = end.getDescriptorForType().findFieldByName("exit_code");
if (end.hasField(ec)) { // ← also false when exit_code == 0
exit = ...;
}
Failure chain
command succeeds → exitCode = 0
→ setField(exit_code, 0) leaves getAllFields() empty (proto3 default-value semantics)
→ end event not attached to event
→ event not attached to response
→ drainStartStream sees no "event" field → continue → frame skipped
→ loop ends with exit == Integer.MIN_VALUE
→ ExecResult.ok() == false → ExecException thrown
Why this has gone unnoticed
E2bCodec.PROTO is the default. The protobuf path uses DynamicMessage.parseFrom() and never touches parseJsonStartResponse(), so the bug is invisible unless JSON codec is explicitly selected.
JSON codec becomes mandatory when the E2B-compatible backend does not accept application/connect+proto. For example, Alibaba Cloud FC Agent Sandbox (envd 0.5.2) returns:
HTTP 400: invalid character '/' after top-level value
for protobuf-encoded requests, forcing JSON codec — at which point the bug blocks all usage.
Reproduction
E2bSandboxClientOptions options = new E2bSandboxClientOptions();
options.setApiKey(System.getenv("E2B_API_KEY"));
options.setApiBaseUrl("<any E2B-compatible endpoint>");
options.setDomain("<matching domain>");
options.setTemplateId("code-interpreter-v1");
options.setCodec(E2bCodec.JSON); // ← the trigger
Sandbox sandbox = new E2bSandboxClient(options, null)
.create(new WorkspaceSpec(), null, options);
sandbox.start(); // fails here: doSetupWorkspace() runs "mkdir -p ..." which exits 0
start() throws before any user command runs, because doSetupWorkspace() issues mkdir -p {workspaceRoot}, which exits 0.
Actual server response (captured from Alibaba Cloud FC, Content-Type application/connect+json)
Connect Protocol frames, 1-byte flags + 4-byte big-endian length + payload:
frame 1 flags=00 len=0x1d {"event":{"start":{"pid":2}}}
frame 2 flags=00 len=0x28 {"event":{"data":{"stdout":"aGVsbG8K"}}}
frame 3 flags=00 len=0x40 {"event":{"end":{"exitCode":0,"exited":true,"status":"exited"}}}
frame 4 flags=02 len=0x02 {}
The response is well-formed and spec-compliant. Frame 3 carries exitCode: 0, which is exactly what triggers the bug.
Fix
Three changes, all in E2bEnvdProcessClient:
// 1 & 2 — parseJsonStartResponse(): drop the getAllFields().isEmpty() guards.
// If the JSON contains the node, the field must be set unconditionally.
// before:
// if (!endBuilder.getAllFields().isEmpty()) {
// event.setField(processEventDesc.findFieldByName("end"), endBuilder.build());
// }
// after:
event.setField(processEventDesc.findFieldByName("end"), endBuilder.build());
// before:
// if (!event.getAllFields().isEmpty()) {
// response.setField(startResponseDesc.findFieldByName("event"), event.build());
// }
// after:
response.setField(startResponseDesc.findFieldByName("event"), event.build());
// 3 — drainStartStream(): hasField() → null check.
// For scalar fields, getField() returns the default value even when "unset".
// before:
// if (end.hasField(ec)) {
// after:
if (ec != null) {
Object v = end.getField(ec);
exit = v instanceof Integer ? (Integer) v : ((Long) v).intValue();
}
Verified end-to-end against Alibaba Cloud FC Agent Sandbox after the fix:
Sandbox created + started in 2123 ms
echo hello → exit=0, stdout=[hello] 43 ms
python3 --version → stdout=[Python 3.13.13] 57 ms
Sandbox killed 52 ms
Environment
agentscope-extensions-sandbox-e2bE2bEnvdProcessClientE2bSandboxClientOptions.setCodec(E2bCodec.JSON)Summary
When
E2bCodec.JSONis selected, any command that exits successfully (exit code 0) throws:-2147483648isInteger.MIN_VALUE, the initial value of theexitvariable indrainStartStream()— meaning noProcessEvent.endframe was ever parsed.Counter-intuitively, commands that fail (non-zero exit code) work correctly.
Root cause
proto3 semantics: for scalar fields (int32, bool, string...), when the value equals the default (0 for int32), the field is treated as unset:
message.getAllFields()does not include itmessage.hasField(field)returns falseparseJsonStartResponse()usesgetAllFields().isEmpty()to decide whether a message has content:Then in
drainStartStream():And a second instance of the same class of bug:
Failure chain
Why this has gone unnoticed
E2bCodec.PROTOis the default. The protobuf path usesDynamicMessage.parseFrom()and never touchesparseJsonStartResponse(), so the bug is invisible unless JSON codec is explicitly selected.JSON codec becomes mandatory when the E2B-compatible backend does not accept
application/connect+proto. For example, Alibaba Cloud FC Agent Sandbox (envd 0.5.2) returns:for protobuf-encoded requests, forcing JSON codec — at which point the bug blocks all usage.
Reproduction
start()throws before any user command runs, becausedoSetupWorkspace()issuesmkdir -p {workspaceRoot}, which exits 0.Actual server response (captured from Alibaba Cloud FC, Content-Type
application/connect+json)Connect Protocol frames, 1-byte flags + 4-byte big-endian length + payload:
The response is well-formed and spec-compliant. Frame 3 carries
exitCode: 0, which is exactly what triggers the bug.Fix
Three changes, all in
E2bEnvdProcessClient:Verified end-to-end against Alibaba Cloud FC Agent Sandbox after the fix: