# 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"
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)
# 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)
# 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:
GET /v2/sandboxes/snapshots?name_contains=<deployment>--setup-<hash>— look for an already-baked recipe snapshotPOST /v2/sandboxes/snapshots— build a "dockerbase" snapshot from the declareddocker_image, which returns anidPOST /v2/sandboxes/boxes— start the bake-builder sandbox on that dockerbase, then runsetup.shin 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:
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— includingDATABASE_URL, provider API keys and third-party tokens — on a box whose only job is to runsetup.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-deepagentslistsHomepageandRepositoryashttps://github.com/langchain-ai/managed-deepagents-sdk, which 404s. The live repo islangchain-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
Source: langchain-ai/deepagents