Files
deepagents/libs/cli/deepagents_cli/config.py
Victor Moreira 72aef52542 feat(cli)!: migrate deepagents deploy to use managed deep agents api [AB-2470] (#3609)
## ⚠ Breaking changes

`deepagents-cli` `0.1.x` → `0.2.0`. The deploy CLI is a private-preview
beta, but
this rewrites the command surface, config format, and deploy backend, so
existing
projects and scripts will not work unchanged.

| What broke | Before (`0.1.x`) | After (`0.2.0`) |
| --- | --- | --- |
| **`deepagents dev` removed** | `deepagents dev --port 2024` ran a
local `langgraph dev` server off the bundle | No replacement. Local
bundling/serving is gone; the project is now a managed agent. |
| **Deploy backend** | `deepagents deploy` bundled the project and
shipped via `langgraph deploy` to LangSmith Deployment | `deepagents
deploy` patches agent metadata + syncs source files through the Managed
Deep Agents API (`/v1/deepagents/*`) |
| **Config file** | `deepagents.toml` (auto-discovered or `--config`) |
`agent.json` (metadata, top-level `backend`, `runtime.model`,
permissions) + `AGENTS.md`, `tools.json`, `skills/`, `subagents/` |
| **`--config` / `--dry-run` flags** | accepted on `deploy`/`dev` |
removed |
| **MCP tool wiring** | prior tool-config shape (deployed an agent with
**no tools**) | `tools.json` with `tools[].mcp_server_url`; servers must
be registered and `can_invoke == true` or deploy fails fast |
| **Deploy state** | derived from the bundle | tracked user-local in
`~/.deepagents/deployments/`, keyed by project root + endpoint (not
committed) |

### Migration

1. **Re-scaffold or convert the project.** Run `deepagents init` to
generate the
   canonical layout (`agent.json`, `AGENTS.md`, `tools.json`, `skills/`,
   `subagents/`), then port settings out of your old `deepagents.toml`:
   - system prompt → `AGENTS.md`
   - model / permissions → `agent.json` (`runtime.model`, `permissions`)
- runtime/sandbox config → top-level `backend` block (`init` scaffolds
`thread_scoped_sandbox`; the legacy value `"sandbox"` is still accepted
as a
     local alias)
2. **Register MCP servers and attach tools.** Use `deepagents
mcp-servers add`
(then `connect` for OAuth servers), and reference them from `tools.json`
via
`tools[].mcp_server_url`. The old tool-config shape silently deployed an
agent
   with no tools.
3. **Replace `deepagents dev`.** There is no local dev-server equivalent
in this
   release; iterate via `deepagents deploy` against your workspace.
4. **Drop removed flags.** Remove `--config` and `--dry-run` from any
deploy
   scripts.

New commands: `deepagents agents` (`get`, `--include-files`, …) and
`deepagents mcp-servers` (`create`/`list`/`get`/`update`/`delete` +
OAuth
`connect`). `get` redacts stored header values.

## Overview

Migrates `deepagents deploy` from the legacy `langgraph deploy` path to
the new
Managed Deep Agents API (`/v1/deepagents/*`). Instead of bundling and
shipping a
deployment, the CLI now treats a project as a managed agent: agent
**metadata**
lives on the agent record and **source files** live in Hub-backed
directory
state. On redeploy, the CLI patches only metadata and syncs the managed
file tree
via Hub directory commits, rather than re-uploading everything.

The CLI surface is now `init` / `deploy` / `agents` / `mcp-servers`,
operating on
the canonical project layout:

```
agent.json              # name, description, top-level backend, runtime.model, permissions
AGENTS.md               # system prompt
tools.json              # MCP tool references (tools[].mcp_server_url) + interrupt_config
skills/<name>/SKILL.md  # frontmatter-tagged skills
subagents/<name>/       # delegated subagent definitions
```

Bumps `deepagents-cli` to `0.2.0`. The deploy flow is gated behind a
beta warning
while Managed Deep Agents is in private preview.

## What changed

**Deploy model**
- Source files are kept in Hub-backed directory state; redeploy patches
agent
metadata only and reconciles files through directory commits (create →
first
  deploy, patch + directory delta → subsequent deploys).
- Deploy state is tracked **user-local** (`~/.deepagents/deployments/`),
keyed by
  project root + endpoint — not committed to the repo.

**Runtime / backend config**
- Managed runtime config moved to a **top-level `backend`** block;
`init` scaffolds
  `thread_scoped_sandbox` by default.
- Backend/sandbox config is validated at load time; legacy `"sandbox"`
is accepted
  as a local alias for `thread_scoped_sandbox`.

**MCP server flows**
- Consistent dotenv loading across all commands (project `.env`
discovered upward,
  then `~/.deepagents/.env`).
- `mcp-servers` supports create/list/get/update/delete plus OAuth
connect; `get`
  redacts stored header values.
- Deploy fails fast when a referenced server is unregistered or the
caller
`can_invoke == false`, with actionable hints (including OAuth
`connect`).

**Tooling docs/examples** (also in this branch)
- `tools.json` (`tools[].mcp_server_url`) is the supported way to attach
MCP tools;
fixed the `deploy-content-writer` example and README quickstart, which
previously
  implied a tool config shape that deploys an agent with no tools.

## Checked with
```bash
uv run --group test python -m pytest tests/unit_tests/deploy -v
uv run --group test ruff check .
uv run --group test ty check
```
Also ran the flow end to end against a live workspace: `init` →
`mcp-servers add`
→ `tools.json` → `deploy` (create + redeploy) → `agents get
--include-files`,
confirming metadata patch, directory sync, and MCP health check.

## Related
- Linear:
https://linear.app/langchain/issue/AB-2470/mda-wrap-apis-in-deepagents-deploy
- Docs PR: `langchain-ai/docs` `vic/mda-cli-update` (reconciles the
Managed Deep
  Agents guide with this behavior)



BREAKING CHANGE: `deepagents deploy` now targets the Managed Deep Agents
API; `deepagents dev`, `deepagents.toml`, and the `--config`/`--dry-run`
flags are removed. Run `deepagents init` to re-scaffold (`agent.json` +
`tools.json`). See
[PR](https://github.com/langchain-ai/deepagents/pull/3609) for the full
migration guide.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2026-06-01 18:28:53 -04:00

172 lines
5.4 KiB
Python

"""Project and global `.env` loading for the deploy CLI."""
from __future__ import annotations
import logging
import os
import sys
from pathlib import Path
logger = logging.getLogger(__name__)
_PROJECT_DOTENV_BLOCKED_ENV_KEYS = (
"LANGSMITH_ENDPOINT",
"LANGCHAIN_ENDPOINT",
"HTTP_PROXY",
"HTTPS_PROXY",
"ALL_PROXY",
"NO_PROXY",
"http_proxy",
"https_proxy",
"all_proxy",
"no_proxy",
"SSL_CERT_FILE",
"SSL_CERT_DIR",
)
def _stderr(msg: str) -> None:
"""Print a diagnostic to stderr without a trailing newline corruption.
The CLI does not configure logging handlers by default, so `logger.warning`
output is invisible to end users. Diagnostics that the user authored — like
a malformed `.env` they meant to load — should also surface on stderr.
"""
sys.stderr.write(f"warning: {msg}\n")
def _find_dotenv_from_start_path(start_path: Path) -> Path | None:
"""Find the nearest `.env` file from an explicit start path upward.
Args:
start_path: Directory to start searching from.
Returns:
Path to the nearest `.env` file, or `None` if not found.
"""
current = start_path.expanduser().resolve()
for parent in [current, *list(current.parents)]:
candidate = parent / ".env"
try:
if candidate.is_file():
return candidate
except OSError:
logger.warning("Could not inspect .env candidate %s", candidate)
continue
return None
def _snapshot_blocked_project_env() -> dict[str, str | None]:
return {key: os.environ.get(key) for key in _PROJECT_DOTENV_BLOCKED_ENV_KEYS}
def _restore_env(snapshot: dict[str, str | None]) -> None:
for key, value in snapshot.items():
if value is None:
os.environ.pop(key, None)
else:
os.environ[key] = value
def _warn_if_project_blocked_env_changed(snapshot: dict[str, str | None]) -> None:
changed = [key for key, value in snapshot.items() if os.environ.get(key) != value]
if not changed:
return
keys = ", ".join(sorted(changed))
_stderr(
f"ignoring {keys} from project .env. Set endpoint overrides in your "
"shell environment or ~/.deepagents/.env; proxy/TLS settings are "
"ignored for managed API requests.",
)
try:
_GLOBAL_DOTENV_PATH = Path.home() / ".deepagents" / ".env"
except RuntimeError:
# `Path.home()` raises when `HOME` is unset/misconfigured (e.g. on some
# CI runners). Fall back to a path that won't exist so `is_file()`
# short-circuits cleanly, and surface the suppression on stderr so the
# user knows global defaults will not apply.
_GLOBAL_DOTENV_PATH = Path("/nonexistent/.deepagents/.env")
_stderr(
"could not resolve home directory; "
"global `~/.deepagents/.env` will not be loaded",
)
logger.warning(
"Could not resolve home directory; global .env will not be loaded",
exc_info=True,
)
def _load_dotenv(*, start_path: Path) -> bool:
"""Load environment variables from project and global `.env` files.
Loads in order (first write wins, `override=False`):
1. Project `.env` — discovered upward from `start_path`.
2. `~/.deepagents/.env` — global user defaults.
Both layers use `override=False` so that shell-exported variables always
take precedence over dotenv files. Because the project file loads first,
the effective precedence is:
```text
shell env > project `.env` > global `.env`
```
When a *resolved* dotenv path fails to parse the user is notified on
stderr in addition to the structured `logger.warning` (the CLI does not
install logging handlers by default).
Args:
start_path: Directory to use for project `.env` discovery. Searched
upward until either a `.env` is found or the filesystem root is
reached.
Returns:
`True` when at least one dotenv file was loaded, `False` otherwise.
"""
import dotenv
loaded = False
dotenv_path = _find_dotenv_from_start_path(start_path)
if dotenv_path is not None:
blocked_env = _snapshot_blocked_project_env()
try:
loaded = (
dotenv.load_dotenv(dotenv_path=dotenv_path, override=False) or loaded
)
_warn_if_project_blocked_env_changed(blocked_env)
except (OSError, ValueError) as exc:
_stderr(
f"could not read project .env at {dotenv_path}: {exc}. "
"Project env vars will not be loaded.",
)
logger.warning(
"Could not read project dotenv at %s; "
"project env vars will not be loaded",
dotenv_path,
exc_info=True,
)
finally:
_restore_env(blocked_env)
try:
if _GLOBAL_DOTENV_PATH.is_file() and dotenv.load_dotenv(
dotenv_path=_GLOBAL_DOTENV_PATH, override=False
):
loaded = True
logger.debug("Loaded global dotenv: %s", _GLOBAL_DOTENV_PATH)
except (OSError, ValueError) as exc:
_stderr(
f"could not read global .env at {_GLOBAL_DOTENV_PATH}: {exc}. "
"Global defaults will not be applied.",
)
logger.warning(
"Could not read global dotenv at %s; global defaults will not be applied",
_GLOBAL_DOTENV_PATH,
exc_info=True,
)
return loaded