diff --git a/libs/cli/langgraph_cli/config.py b/libs/cli/langgraph_cli/config.py index 5fdfc6cfb..84c8425f9 100644 --- a/libs/cli/langgraph_cli/config.py +++ b/libs/cli/langgraph_cli/config.py @@ -381,7 +381,9 @@ def _get_source_kind(config: Config) -> str | None: return kind if isinstance(kind, str) else None -def validate_config(config: Config) -> Config: +def validate_config( + config: Config, *, source_path: pathlib.Path | None = None +) -> Config: """Validate a configuration dictionary.""" graphs = config.get("graphs", {}) @@ -477,9 +479,12 @@ def validate_config(config: Config) -> Config: ) _validate_git_http_url_userinfo( - dependency - for dependency in config["dependencies"] - if isinstance(dependency, str) + ( + dependency + for dependency in config["dependencies"] + if isinstance(dependency, str) + ), + source=source_path, ) source = config.get("source") @@ -676,7 +681,7 @@ def validate_config_file(config_path: pathlib.Path) -> Config: """Load and validate a configuration file.""" with open(config_path) as f: config = json.load(f) - validated = validate_config(config) + validated = validate_config(config, source_path=config_path.resolve()) # Enforce the package.json doesn't enforce an # incompatible Node.js version if validated.get("node_version"): diff --git a/libs/cli/tests/unit_tests/test_config.py b/libs/cli/tests/unit_tests/test_config.py index bcd8c8c1c..c69157458 100644 --- a/libs/cli/tests/unit_tests/test_config.py +++ b/libs/cli/tests/unit_tests/test_config.py @@ -280,6 +280,28 @@ def test_validate_config_rejects_git_http_url_userinfo(dependency: str): assert "secret%2Ftoken" not in message +def test_validate_config_file_reports_source_for_git_http_url_userinfo( + tmp_path: pathlib.Path, +): + config_path = tmp_path / "langgraph.json" + config_path.write_text( + json.dumps( + { + "python_version": "3.11", + "dependencies": ["git+https://secret-token@github.com/org/private.git"], + "graphs": {"agent": "./agent.py:graph"}, + } + ) + ) + + with pytest.raises(click.UsageError) as exc_info: + validate_config_file(config_path) + + message = str(exc_info.value) + assert "secret-token" not in message + assert f"Found in: {config_path.resolve()}" in message + + @pytest.mark.parametrize( "manifest", ["package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml"] )