Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
## Other Changes
- Minor improvements to the `agent` command and related utilities.
## Features
- Added network egress controls support in local development environments (localstack).
21 changes: 12 additions & 9 deletions dispatch_cli/mcp/operator/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

from dispatch_cli.utils import (
DISPATCH_YAML,
DISPATCH_YAML_HIDDEN,
LOCAL_ROUTER_PORT,
LOCAL_ROUTER_URL,
)
Expand Down Expand Up @@ -908,18 +907,22 @@ def _get_namespace(namespace: str | None = None) -> str:
return str(ns)

def _read_agent_config(agent_directory: str) -> dict[str, Any]:
"""Read dispatch.yaml (or .dispatch.yaml) from an agent directory.
"""Read dispatch.yaml from an agent directory.

Returns the parsed YAML as a dict, or {} if no config file found.
"""
import yaml

for name in (DISPATCH_YAML, DISPATCH_YAML_HIDDEN):
candidate = os.path.join(agent_directory, name)
if os.path.exists(candidate):
with open(candidate, encoding="utf-8") as fh:
data = yaml.safe_load(fh) or {}
return data if isinstance(data, dict) else {}
hidden = os.path.join(agent_directory, ".dispatch.yaml")
if os.path.exists(hidden):
raise RuntimeError(
".dispatch.yaml is no longer supported; rename it to dispatch.yaml"
)
candidate = os.path.join(agent_directory, DISPATCH_YAML)
if os.path.exists(candidate):
with open(candidate, encoding="utf-8") as fh:
data = yaml.safe_load(fh) or {}
return data if isinstance(data, dict) else {}
return {}

@mcp.tool()
Expand Down Expand Up @@ -2107,7 +2110,7 @@ def local_agent_dev() -> str:
".tox",
".mypy_cache",
}
dispatch_names = {DISPATCH_YAML, DISPATCH_YAML_HIDDEN}
dispatch_names = {DISPATCH_YAML}

for dirpath, dirnames, filenames in os.walk(cwd):
depth = Path(dirpath).relative_to(cwd).parts
Expand Down
6 changes: 2 additions & 4 deletions dispatch_cli/templates/extract_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
if not os.path.exists("/app"):
print("Warning: /app folder not found, assuming local development")
# Check if we're running from project root (dispatch.yaml exists in current dir)
if os.path.exists("./dispatch.yaml") or os.path.exists("./.dispatch.yaml"):
if os.path.exists("./dispatch.yaml"):
root_path = "."
else:
# Fallback to parent directory (this shouldn't typically happen)
Expand Down Expand Up @@ -50,12 +50,10 @@
def extract_schemas_and_compliance():
"""Extract handler schemas and check typing compliance."""
try:
# Read entrypoint from dispatch.yaml (or .dispatch.yaml)
# Read entrypoint from dispatch.yaml
import yaml

config_path = os.path.join(root_path, "dispatch.yaml")
if not os.path.exists(config_path):
config_path = os.path.join(root_path, ".dispatch.yaml")
with open(config_path) as f:
config = yaml.safe_load(f)

Expand Down
4 changes: 1 addition & 3 deletions dispatch_cli/templates/grpc_listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
if not os.path.exists("/app"):
print("Warning: /app folder not found, assuming local development", flush=True)
# Check if we're running from project root (dispatch.yaml exists in current dir)
if os.path.exists("./dispatch.yaml") or os.path.exists("./.dispatch.yaml"):
if os.path.exists("./dispatch.yaml"):
root_path = "."
else:
# Fallback to parent directory (this shouldn't typically happen)
Expand Down Expand Up @@ -59,8 +59,6 @@ def load_config():
with open(pyproject_path, "rb") as f:
pyproject = tomlkit.load(f)
yaml_path = os.path.join(root_path, "dispatch.yaml")
if not os.path.exists(yaml_path):
yaml_path = os.path.join(root_path, ".dispatch.yaml")
if os.path.exists(yaml_path):
try:
with open(yaml_path, encoding="utf-8") as f:
Expand Down
41 changes: 17 additions & 24 deletions dispatch_cli/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@

DISPATCH_DIR = ".dispatch"
DISPATCH_YAML = "dispatch.yaml"
DISPATCH_YAML_HIDDEN = ".dispatch.yaml" # backward compat
DISPATCH_LISTENER_MODULE = "__dispatch_listener__"
DISPATCH_LISTENER_FILE = f"{DISPATCH_LISTENER_MODULE}.py"

Expand Down Expand Up @@ -127,6 +126,7 @@ def get_sdk_dependency() -> str:
"volumes": None, # list of volume objects (like [{"name": "data", "mountPath": "/data", "mode": "read_write_many"}])
"mcp_servers": None, # list of MCP server configs (e.g., [{"server": "com.datadoghq.mcp"}])
"resources": None, # resource limits (like {"cpu": 512, "memory": 1024})
"network": None, # network egress restrictions (like {"egress": {"allow_domains": [{"match_name": "api.openai.com"}]}})
}


Expand Down Expand Up @@ -188,20 +188,20 @@ def read_project_config(


def _find_dispatch_yaml(path: str) -> str | None:
"""Return the path to the dispatch config file, or None if not found.

Prefers ``dispatch.yaml``; falls back to ``.dispatch.yaml`` for
backward compatibility.
"""
for name in (DISPATCH_YAML, DISPATCH_YAML_HIDDEN):
candidate = os.path.join(path, name)
if os.path.exists(candidate):
return candidate
"""Return the path to the dispatch config file, or None if not found."""
hidden = os.path.join(path, ".dispatch.yaml")
if os.path.exists(hidden):
raise RuntimeError(
".dispatch.yaml is no longer supported; rename it to dispatch.yaml"
)
candidate = os.path.join(path, DISPATCH_YAML)
if os.path.exists(candidate):
return candidate
return None


def read_dispatch_yaml(path: str) -> dict:
"""Read configuration overrides from dispatch.yaml (or .dispatch.yaml)."""
"""Read configuration overrides from dispatch.yaml."""
yaml_path = _find_dispatch_yaml(path)
if yaml_path is None:
return {}
Expand Down Expand Up @@ -229,7 +229,7 @@ def read_dispatch_yaml(path: str) -> dict:


def save_dispatch_yaml(path: str, config: dict) -> None:
"""Persist configuration values to dispatch.yaml (or .dispatch.yaml if that's what exists)."""
"""Persist configuration values to dispatch.yaml."""
# Write to whichever file already exists; default to dispatch.yaml for new projects
yaml_path = _find_dispatch_yaml(path) or os.path.join(path, DISPATCH_YAML)
payload = _config_for_yaml(config)
Expand Down Expand Up @@ -702,24 +702,17 @@ def validate_dispatch_project(path: str) -> bool:
)
return False

# Check for dispatch config file conflicts and deprecation
# Check for dispatch config file
has_visible = os.path.exists(os.path.join(path, DISPATCH_YAML))
has_hidden = os.path.exists(os.path.join(path, DISPATCH_YAML_HIDDEN))
has_hidden = os.path.exists(os.path.join(path, ".dispatch.yaml"))

if has_visible and has_hidden:
if has_hidden:
logger.error(
f"Found both {DISPATCH_YAML} and {DISPATCH_YAML_HIDDEN}. "
f"Please remove {DISPATCH_YAML_HIDDEN} and keep only {DISPATCH_YAML}."
".dispatch.yaml is no longer supported; rename it to dispatch.yaml"
)
return False

if has_hidden and not has_visible:
logger.warning(
f"{DISPATCH_YAML_HIDDEN} is deprecated and will be removed in a future release. "
f"Please rename it to {DISPATCH_YAML}."
)

if not has_visible and not has_hidden:
if not has_visible:
logger.error(
f"{DISPATCH_YAML} not found. "
"Run 'dispatch agent init' to regenerate project assets."
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "dispatch-cli"
version = "0.7.4"
version = "0.8.0"
description = ""
authors = [
{name = "Diamond Bishop", email = "diamond.bishop@datadoghq.com"},
Expand Down
2 changes: 1 addition & 1 deletion tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ def test_returns_true_when_files_exist(self):
with open(os.path.join(tmpdir, "pyproject.toml"), "w") as f:
f.write("[tool.dispatch]\nentrypoint = 'agent.py'\n")
# create the dispatch.yaml file (required by validate_dispatch_project)
with open(os.path.join(tmpdir, ".dispatch.yaml"), "w") as f:
with open(os.path.join(tmpdir, "dispatch.yaml"), "w") as f:
f.write("entrypoint: agent.py\nnamespace: test\n")
# create the Dockerfile file
Path(os.path.join(dispatch_dir, "Dockerfile")).touch()
Expand Down
14 changes: 7 additions & 7 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading