Skip to content

feat: Add structured exit_code, upload_file, download_file tools for BaseSandbox compatibility #3

Description

@ryaneggz

Summary

The Orchestra project needs to use exec_server as a DeepAgents BaseSandbox backend. The current exec_server has compatibility gaps that need to be addressed in ubuntu/index.js.

Orchestra spec: ruska-ai/orchestraspecs/spec-d-mcp-sandbox.md (section: "Required exec_server Changes")

Gap Analysis

What BaseSandbox needs exec_server currently provides Gap
ExecuteResponse(output, exit_code: int, truncated: bool) Single text blob: "stdout:\n...\nstderr:\n...\nexit_code: N" exit_code embedded in text, not structured
upload_files([(path, bytes)]) Not supported No file upload tool
download_files([path]) Not supported No file download tool
id property Not exposed No sandbox identifier
Tool param: cmd Tool param: cmd Match

Key insight: BaseSandbox's inherited methods (read, write, edit, grep_raw, glob_info, ls_info) construct Python heredoc shell commands and pass them to execute(). They depend on exit_code being a proper integer field — they do not parse it from output text.

Required Changes

a) Return structured exit_code

Change execCommandHandler to return exit_code as structured _meta metadata, not embedded in text:

// Current (text-embedded):
output.push(`exit_code: ${error.code ?? 1}`);

// Proposed (structured via _meta):
return {
  content: [
    { type: "text", text: output.join("\n") || "(no output)" }
  ],
  _meta: {
    exit_code: error?.code ?? 0,
    truncated: false
  }
};

Text output should contain only stdout/stderr, not the exit_code: line.

b) Add upload_file tool

server.tool("upload_file", "Write base64-encoded content to a file",
  { path: z.string(), content_base64: z.string() },
  async ({ path, content_base64 }) => {
    const dir = require("path").dirname(path);
    await fs.promises.mkdir(dir, { recursive: true });
    const buffer = Buffer.from(content_base64, "base64");
    await fs.promises.writeFile(path, buffer);
    return {
      content: [{ type: "text", text: `Written ${buffer.length} bytes to ${path}` }],
      _meta: { exit_code: 0 }
    };
  }
);

c) Add download_file tool

server.tool("download_file", "Read a file and return as base64",
  { path: z.string() },
  async ({ path }) => {
    const buffer = await fs.promises.readFile(path);
    return {
      content: [{ type: "text", text: buffer.toString("base64") }],
      _meta: { exit_code: 0 }
    };
  }
);

d) Expose sandbox ID

Include a unique identifier in the initialize response's serverInfo:

serverInfo: {
  name: "exec-server",
  version: "1.0.0",
  sandbox_id: process.env.SANDBOX_ID || `sandbox-${process.pid}`
}

Migration Path

  1. Phase 1 (now): Orchestra adapter parses text-embedded exit_code and uses shell fallbacks for file ops — works with current exec_server unchanged.
  2. Phase 2: Update exec_server with structured _meta, upload_file, and download_file tools — Orchestra adapter automatically prefers these when available.
  3. Phase 3: Remove text-embedded exit_code from exec_server output once all consumers are updated.

Acceptance Criteria

  • exec_command returns _meta.exit_code as structured integer (not in text)
  • upload_file tool available for base64 file writes
  • download_file tool available for base64 file reads
  • Sandbox ID exposed in initialize response
  • Backward compatibility: existing consumers still work during transition

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions