diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 64a3775..bd0816a 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -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). diff --git a/dispatch_cli/mcp/operator/tools.py b/dispatch_cli/mcp/operator/tools.py index 03fcb6b..34f4ca9 100644 --- a/dispatch_cli/mcp/operator/tools.py +++ b/dispatch_cli/mcp/operator/tools.py @@ -14,7 +14,6 @@ from dispatch_cli.utils import ( DISPATCH_YAML, - DISPATCH_YAML_HIDDEN, LOCAL_ROUTER_PORT, LOCAL_ROUTER_URL, ) @@ -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() @@ -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 diff --git a/dispatch_cli/templates/extract_schemas.py b/dispatch_cli/templates/extract_schemas.py index 7fc75e8..bd02318 100644 --- a/dispatch_cli/templates/extract_schemas.py +++ b/dispatch_cli/templates/extract_schemas.py @@ -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) @@ -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) diff --git a/dispatch_cli/templates/grpc_listener.py b/dispatch_cli/templates/grpc_listener.py index 1d1c7e8..d9ae8c3 100644 --- a/dispatch_cli/templates/grpc_listener.py +++ b/dispatch_cli/templates/grpc_listener.py @@ -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) @@ -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: diff --git a/dispatch_cli/utils.py b/dispatch_cli/utils.py index fcaf040..525d73e 100644 --- a/dispatch_cli/utils.py +++ b/dispatch_cli/utils.py @@ -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" @@ -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"}]}}) } @@ -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 {} @@ -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) @@ -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." diff --git a/pyproject.toml b/pyproject.toml index c80c5bf..6a660b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"}, diff --git a/tests/test_utils.py b/tests/test_utils.py index dabcdbd..c2786c2 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -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() diff --git a/uv.lock b/uv.lock index 16d6140..7e5cdf7 100644 --- a/uv.lock +++ b/uv.lock @@ -557,7 +557,7 @@ wheels = [ [[package]] name = "dispatch-agents" -version = "0.10.1" +version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -570,14 +570,14 @@ dependencies = [ { name = "pyyaml" }, { name = "tomlkit" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/39/824d1fe797a1b47f9dc31822db6eeec9612ad1613f7fd1666c31048b7beb/dispatch_agents-0.10.1.tar.gz", hash = "sha256:29bc4f7027a3b95c1f8f3b5ebc1897a62232b13a85c22614d2620169726d4118", size = 562020, upload-time = "2026-03-27T20:22:50.58Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/c1/bb9f3159c10b2912c8855e4ef14412ed7490dea6778c5b556cad184ba9aa/dispatch_agents-0.11.0.tar.gz", hash = "sha256:6bef801ac2d8cbc7759ae336d6d03951c1568467da965db4a7a63b2c47e766cf", size = 564841, upload-time = "2026-03-30T20:15:38.227Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/67/e29c7f5876fc5c09f3507fe676415343163132ce7da2947df2f113825856/dispatch_agents-0.10.1-py3-none-any.whl", hash = "sha256:08d3509873bb84dbd8212371a5b877f2e58fb745288da0e12ca439b89d04be31", size = 105785, upload-time = "2026-03-27T20:22:49.588Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3d/8761a9f24b4622aa28d143e1ca560d8b6a03b2060a198208bb0a98b24080/dispatch_agents-0.11.0-py3-none-any.whl", hash = "sha256:c69aeb7437956b073f54f75c2804d4a132b3004310a2785929e254bf89a01421", size = 107780, upload-time = "2026-03-30T20:15:37.289Z" }, ] [[package]] name = "dispatch-cli" -version = "0.7.4" +version = "0.8.0" source = { editable = "." } dependencies = [ { name = "aiohttp" }, @@ -1735,7 +1735,7 @@ wheels = [ [[package]] name = "requests" -version = "2.33.0" +version = "2.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -1743,9 +1743,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] [[package]]