Skip to content

E2bCodec.JSON mode fails on every successful command due to proto3 default-value semantics #2601

Description

@zylgq

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions