#6361·deepagents

# Sandbox bake sends both snapshot_name and snapshot_id on the bake-builder create, and the API rejects it with 422 "snapshot_id and snapshot are mutually exclusive"

Author: Pratham271Created Sep 17, 2026Updated Sep 17, 2026
Labelstype:bugpackage:deepagentsorg:externalpriority:backlogtopic:sandboxes

Submission checklist

  • This is a bug, not a usage question.
  • I added a clear and descriptive title.
  • I searched existing issues and didn't find this.
  • I can reproduce this with the latest released version.
  • I included a minimal reproducible example and steps to reproduce.

Area (Required)

  • deepagents (SDK)
  • dcode
  • talon
  • acp
  • evals
  • daytona
  • modal
  • quickjs
  • runloop
  • vercel
  • langsmith-sandbox
  • Other / not sure / general

Related Issues / PRs

No response

Reproduction Steps / Example Code (Python)

python
# Minimal reproduction of the mda sandbox-bake 422.
#
# Manual steps, if you prefer to run them by hand against a real workspace:
#
#   1. pip install managed-deepagents        (reproduced on 0.6.1 and 0.7.2)
#   2. mda init mdabug --memory none
#   3. cd mdabug
#   4. echo 'echo hello from setup.sh' > sandbox/setup.sh
#   5. replace sandbox/__init__.py with:
#
#        from managed_deepagents import define_sandbox
#
#        sandbox = define_sandbox(docker_image="python:3.13-slim", idle_ttl_seconds=600)
#
#   6. mda dev
#
#   -> error: failed to create bake builder '...--bake-...':
#      snapshot_id and snapshot are mutually exclusive (HTTP 422)
#
# The script below automates exactly those steps and needs NO LangSmith account:
# a local mock stands in for the sandbox API, so the offending request is
# captured and printed instead of guessed at. Standard library only.
#
#   pip install managed-deepagents
#   python repro.py

import json
import os
import shutil
import subprocess
import sys
import tempfile
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path

PORT = 8899
DOCKER_IMAGE = "python:3.13-slim"
CAPTURED = {}


class MockSandboxAPI(BaseHTTPRequestHandler):
    """The handful of endpoints the bake walks through, in order."""

    def _reply(self, status, payload):
        body = json.dumps(payload).encode()
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def _handle(self):
        length = int(self.headers.get("Content-Length") or 0)
        raw = self.rfile.read(length) if length else b""
        path = self.path.split("?")[0]

        # Step 3: the bake-builder create. This is the bug.
        if self.command == "POST" and path.endswith("/v2/sandboxes/boxes"):
            CAPTURED["body"] = json.loads(raw)
            return self._reply(422, {"detail": "snapshot_id and snapshot are mutually exclusive"})

        # Step 1: no recipe snapshot exists yet, so the bake proceeds.
        if self.command == "GET" and path.endswith("/v2/sandboxes/snapshots"):
            return self._reply(200, {"snapshots": [], "total": 0})

        # Step 2: building the dockerbase snapshot hands the CLI an id.
        if path.startswith("/v2/sandboxes/snapshots"):
            return self._reply(
                200,
                {"id": "snapshot-id-from-step-2", "name": "dockerbase", "status": "ready"},
            )

        return self._reply(200, {})

    do_GET = do_POST = do_PUT = do_PATCH = do_DELETE = _handle

    def log_message(self, *args):
        pass


def scaffold(mda, root):
    """mda init, plus the two edits that turn the bake on and make it fail."""
    subprocess.run(
        [mda, "init", "mdabug", "--memory", "none"],
        cwd=root,
        check=True,
        capture_output=True,
        text=True,
    )
    project = root / "mdabug"

    # A recipe script is what makes mda dev bake at all.
    (project / "sandbox" / "setup.sh").write_text(
        "#!/usr/bin/env bash\nset -euo pipefail\necho hello from setup.sh\n"
    )

    # docker_image is the only bake base that triggers the 422.
    (project / "sandbox" / "__init__.py").write_text(
        "from managed_deepagents import define_sandbox\n"
        "\n"
        'sandbox = define_sandbox(docker_image="%s", idle_ttl_seconds=600)\n' % DOCKER_IMAGE
    )
    return project


def main():
    mda = shutil.which("mda")
    if mda is None:
        sys.exit("mda is not on PATH. Run: pip install managed-deepagents")

    server = HTTPServer(("127.0.0.1", PORT), MockSandboxAPI)
    threading.Thread(target=server.serve_forever, daemon=True).start()

    try:
        with tempfile.TemporaryDirectory() as tmp:
            project = scaffold(mda, Path(tmp))
            env = dict(os.environ)
            env["LANGSMITH_ENDPOINT"] = "http://127.0.0.1:%d" % PORT
            env["LANGSMITH_API_KEY"] = "lsv2_pt_mock"
            run = subprocess.run(
                [mda, "dev", "--no-browser"],
                cwd=project,
                env=env,
                capture_output=True,
                text=True,
                timeout=300,
            )
    finally:
        server.shutdown()

    print("--- mda output " + "-" * 49)
    print((run.stdout + run.stderr).strip())

    body = CAPTURED.get("body")
    if body is None:
        sys.exit("\nNo POST /v2/sandboxes/boxes captured; the bake did not get that far.")

    # The CLI also ships the whole project env on this box; not the point here.
    body.pop("env_vars", None)
    print("\n--- POST /v2/sandboxes/boxes " + "-" * 35)
    print(json.dumps(body, indent=2, sort_keys=True))

    bases = sorted(k for k in ("snapshot_name", "snapshot_id") if k in body)
    print("\nbake bases sent: %s" % bases)
    if len(bases) == 1:
        print("OK - exactly one bake base. Not reproduced.")
        return 0
    print("BUG - expected exactly one bake base, got both.")
    return 1


if __name__ == "__main__":
    sys.exit(main())

Error Message and Stack Trace (if applicable)

bash
# The original failure, against the real API (uv run mda dev):

┌   mda dev
│
●  Project: /Users/me/paid-media-agent (Python)
│
◇  Compiled 234 file(s) → .mda/build
│
◓  Baking sandbox recipe snapshot
▲  Could not bake the sandbox recipe

error: failed to create bake builder 'dev-28d6f7f1608de6d5--bake-50654120b04cdffe': snapshot_id and snapshot are mutually exclusive (HTTP 422)


# The same failure from the repro script above, which also prints the request body.
# Exit code 1.

--- mda output -------------------------------------------------
┌   mda dev
│
●  Project: /private/var/folders/.../tmpyh3l4wkz/mdabug (Python)
│
Sandbox bake: checking for a ready recipe snapshot
Sandbox bake: building snapshot from Docker image
Sandbox bake: starting builder sandbox
error: failed to create bake builder 'dev-1ae266a1fd5ce2be--bake-44a21248f224461f': snapshot_id and snapshot are mutually exclusive (HTTP 422)

--- POST /v2/sandboxes/boxes -----------------------------------
{
  "idle_ttl_seconds": 900,
  "name": "dev-1ae266a1fd5ce2be--bake-44a21248f224461f",
  "snapshot_id": "snapshot-id-from-step-2",
  "snapshot_name": "dev-1ae266a1fd5ce2be--dockerbase-44a21248f224461f",
  "timeout": 180,
  "wait_for_ready": true
}

bake bases sent: ['snapshot_id', 'snapshot_name']
BUG - expected exactly one bake base, got both.

Description

I'm trying to run mda dev on a project whose sandbox/__init__.py declares a docker_image bake base and ships a sandbox/setup.sh.

I expect the recipe to bake and the dev server to come up.

Instead the bake fails immediately with 422 snapshot_id and snapshot are mutually exclusive, and it fails the same way on every run.

What the CLI is doing

The bake is a three-step sequence:

  1. GET /v2/sandboxes/snapshots?name_contains=<deployment>--setup-<hash> — look for an already-baked recipe snapshot
  2. POST /v2/sandboxes/snapshots — build a "dockerbase" snapshot from the declared docker_image, which returns an id
  3. POST /v2/sandboxes/boxes — start the bake-builder sandbox on that dockerbase, then run setup.sh in it

Step 3 sends both the dockerbase's snapshot_name and the snapshot_id returned by step 2. The sandbox API treats snapshot_name as snapshot, and snapshot / snapshot_id are mutually exclusive, so the create 422s and the bake never starts.

The author only ever set one bake base. normalize_sandbox_options in managed_deepagents/sandbox.py enforces that mutual exclusion correctly on the authoring side — the second value is introduced by the CLI itself at step 3.

Only the docker_image base is affected

Probing all four bake bases against a local mock, the colliding pair is unique to docker_image:

define_sandbox(...) box create body carries result
docker_image="python:3.13-slim" snapshot_name and snapshot_id 422
snapshot_id="<uuid>" snapshot_id only accepted
snapshot_name="<name>" snapshot_name only accepted
(no base) neither accepted

The three working cases pass through a base the CLI did not have to create, so there is no second identifier in hand. Only docker_image makes the CLI mint a dockerbase snapshot first, and that is where it ends up holding both the name it chose and the id the API returned — and sends both.

Expected

The bake-builder create sends exactly one bake base. The Python runtime already models this correctly — _sandbox_create_kwargs in managed_deepagents/runtime.py strips every author bake-base key and sets a single snapshot_name from MDA_SANDBOX_RECIPE_SNAPSHOT. The CLI's bake path should follow the same one-of rule.

Workaround

Pre-build the base yourself and declare it by id, so the dockerbase step never runs:

python
sandbox = define_sandbox(snapshot_id="<uuid>", idle_ttl_seconds=1800)

Building that snapshot through langsmith.sandbox.SandboxClient.create_snapshot_from_dockerfile works fine — a different code path that never touches the bake-builder create. The cost is that the base image is no longer declared in the repo, and setup.sh then runs twice: once in the Docker build, once again in MDA's bake on top of it.

Two smaller things noticed while capturing this

  • The bake-builder create carries the project's entire resolved environment as env_vars — including DATABASE_URL, provider API keys and third-party tokens — on a box whose only job is to run setup.sh. A recipe bake that by design must not bake secrets into the image probably does not need the full runtime env. (Stripped from the captured body above.)
  • PyPI metadata for managed-deepagents lists Homepage and Repository as https://github.com/langchain-ai/managed-deepagents-sdk, which 404s. The live repo is langchain-ai/managed-deepagents.

Environment / System Info

OS: macOS 26.5.2, arm64 (Darwin 25.5.0) Python: 3.13.3 managed-deepagents: 0.6.1 (bundled CLI reports: mda 0.0.1 (1adff15 2026-08-25)) also reproduced on: 0.7.2 (bundled CLI reports: mda 0.0.1 (d36bd7e 2026-09-10)) deepagents: 0.7.12 langgraph: 1.2.11 langchain: 1.3.18 uv: 0.11.21 LANGSMITH_ENDPOINT: unset, so the default https://api.smith.langchain.com