From de9b5216c8de9709beaeb10d68f7a00b4859325f Mon Sep 17 00:00:00 2001 From: John Kennedy <65985482+jkennedyvz@users.noreply.github.com> Date: Wed, 5 Aug 2026 22:15:35 +0000 Subject: [PATCH 1/5] fix: reject credential-bearing Git dependencies Co-authored-by: open-swe[bot] --- libs/cli/README.md | 2 ++ libs/cli/langgraph_cli/config.py | 21 +++++++++++ libs/cli/langgraph_cli/schemas.py | 3 ++ libs/cli/tests/unit_tests/test_config.py | 44 ++++++++++++++++++++++++ 4 files changed, 70 insertions(+) diff --git a/libs/cli/README.md b/libs/cli/README.md index cac7808bb09..82016c0b919 100644 --- a/libs/cli/README.md +++ b/libs/cli/README.md @@ -103,6 +103,8 @@ The CLI uses a `langgraph.json` configuration file with these key settings: } ``` +Git dependencies must use credential-free URLs. The CLI rejects HTTP Git URLs with userinfo because generated Dockerfiles and image layers can retain embedded usernames or tokens. For private dependencies, provide short-lived credentials through your build environment's secret-backed Git credential helper. Do not store credentials in `langgraph.json`, requirement or lock files, or a `pip_config_file` copied into the image. + See the [full documentation](https://reference.langchain.com/python/langgraph-cli) for detailed configuration options. ## Development diff --git a/libs/cli/langgraph_cli/config.py b/libs/cli/langgraph_cli/config.py index 57b22518728..ecf3cc8944e 100644 --- a/libs/cli/langgraph_cli/config.py +++ b/libs/cli/langgraph_cli/config.py @@ -36,6 +36,7 @@ # This blocks background execution (cmd &) while allowing command # chaining (cmd1 && cmd2) which is common in build commands. _SINGLE_AMPERSAND_RE = re.compile(r"(?[^/\s]+)", re.I) _API_VERSION_PATTERN = re.compile( r"^(?P\d+)" r"(?:\.(?P\d+))?" @@ -78,6 +79,14 @@ def has_disallowed_build_command_content(command: str) -> bool: return False +def _has_git_http_url_userinfo(dependency: str) -> bool: + """Check whether a Git HTTP URL contains userinfo.""" + return any( + "@" in match.group("authority") + for match in _GIT_HTTP_AUTHORITY_RE.finditer(dependency) + ) + + MIN_PYTHON_VERSION = "3.11" DEFAULT_PYTHON_VERSION = "3.11" @@ -415,6 +424,18 @@ def validate_config(config: Config) -> Config: ' "source": {"kind": "uv", "root": ".."}' ) + if any( + isinstance(dependency, str) and _has_git_http_url_userinfo(dependency) + for dependency in config["dependencies"] + ): + raise click.UsageError( + "Git dependency URLs must not contain credentials or other URL " + "userinfo because generated Dockerfiles and image layers can retain " + "them. Use a credential-free Git URL and provide short-lived " + "credentials through your build environment's secret-backed Git " + "credential helper." + ) + source = config.get("source") source_kind = _get_source_kind(config) if source is not None and not isinstance(source, dict): diff --git a/libs/cli/langgraph_cli/schemas.py b/libs/cli/langgraph_cli/schemas.py index 5a00fc67355..6678d7d6761 100644 --- a/libs/cli/langgraph_cli/schemas.py +++ b/libs/cli/langgraph_cli/schemas.py @@ -689,6 +689,9 @@ class Config(TypedDict, total=False): - "." or "./src" if you have a local Python package - str (aka "anthropic") for a PyPI package - "git+https://github.com/org/repo.git@main" for a Git-based package + Git HTTP URLs must not contain userinfo such as a username or token. For private + dependencies, provide short-lived credentials through the build environment's + secret-backed Git credential helper. Defaults to an empty list, meaning no additional packages installed beyond your base environment. This field is not supported when `source.kind` is `uv`. diff --git a/libs/cli/tests/unit_tests/test_config.py b/libs/cli/tests/unit_tests/test_config.py index c8b24020cf4..3d34f415dcd 100644 --- a/libs/cli/tests/unit_tests/test_config.py +++ b/libs/cli/tests/unit_tests/test_config.py @@ -255,6 +255,50 @@ def test_validate_config(): ) +@pytest.mark.parametrize( + "dependency", + [ + "git+https://user:secret-token@github.com/org/private.git@main", + "private-package @ git+http://token@github.com/org/private.git", + "git+HTTPS://user%40example.com:secret%2Ftoken@github.com/org/private.git", + ], +) +def test_validate_config_rejects_git_http_url_userinfo(dependency: str): + with pytest.raises(click.UsageError) as exc_info: + validate_config( + { + "python_version": "3.11", + "dependencies": [dependency], + "graphs": {"agent": "./agent.py:graph"}, + } + ) + + message = str(exc_info.value) + assert "must not contain credentials or other URL userinfo" in message + assert "secret-token" not in message + assert "secret%2Ftoken" not in message + + +@pytest.mark.parametrize( + "dependency", + [ + "git+https://github.com/org/public.git@main", + "private-package @ git+https://github.com/org/private.git@main", + "git+ssh://git@github.com/org/private.git@main", + ], +) +def test_validate_config_allows_git_urls_without_http_userinfo(dependency: str): + config = validate_config( + { + "python_version": "3.11", + "dependencies": [dependency], + "graphs": {"agent": "./agent.py:graph"}, + } + ) + + assert config["dependencies"] == [dependency] + + def test_validate_config_image_distro(): """Test validation of image_distro field.""" # Valid image_distro values should work From b51ef5770754d2f66b169b8ea53aa9c26ddb8a41 Mon Sep 17 00:00:00 2001 From: John Kennedy <65985482+jkennedyvz@users.noreply.github.com> Date: Wed, 5 Aug 2026 22:36:53 +0000 Subject: [PATCH 2/5] fix: validate nested Git dependency credentials Co-authored-by: open-swe[bot] --- libs/cli/langgraph_cli/config.py | 66 ++++++++++++++++---- libs/cli/langgraph_cli/uv_lock.py | 10 +++ libs/cli/tests/unit_tests/test_config.py | 79 ++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 12 deletions(-) diff --git a/libs/cli/langgraph_cli/config.py b/libs/cli/langgraph_cli/config.py index ecf3cc8944e..aabc824d760 100644 --- a/libs/cli/langgraph_cli/config.py +++ b/libs/cli/langgraph_cli/config.py @@ -6,6 +6,7 @@ import shlex import textwrap from collections import Counter +from collections.abc import Iterable from typing import Literal, NamedTuple import click @@ -36,7 +37,10 @@ # This blocks background execution (cmd &) while allowing command # chaining (cmd1 && cmd2) which is common in build commands. _SINGLE_AMPERSAND_RE = re.compile(r"(?[^/\s]+)", re.I) +_GIT_HTTP_AUTHORITY_RES = ( + re.compile(r"git\+https?://(?P[^/\s\"']+)", re.I), + re.compile(r"\bgit\s*=\s*[\"']https?://(?P[^/\s\"']+)", re.I), +) _API_VERSION_PATTERN = re.compile( r"^(?P\d+)" r"(?:\.(?P\d+))?" @@ -83,10 +87,53 @@ def _has_git_http_url_userinfo(dependency: str) -> bool: """Check whether a Git HTTP URL contains userinfo.""" return any( "@" in match.group("authority") - for match in _GIT_HTTP_AUTHORITY_RE.finditer(dependency) + for pattern in _GIT_HTTP_AUTHORITY_RES + for match in pattern.finditer(dependency) + ) + + +def _validate_git_http_url_userinfo(values: Iterable[str]) -> None: + """Reject credential-bearing Git HTTP URLs without echoing their values.""" + if not any(_has_git_http_url_userinfo(value) for value in values): + return + raise click.UsageError( + "Git dependency URLs must not contain credentials or other URL " + "userinfo because generated Dockerfiles and image layers can retain " + "them. Use a credential-free Git URL and provide short-lived " + "credentials through your build environment's secret-backed Git " + "credential helper." ) +def _validate_git_http_url_userinfo_files(paths: Iterable[pathlib.Path]) -> None: + """Reject credential-bearing Git HTTP URLs in dependency files.""" + contents: list[str] = [] + for path in paths: + if not path.is_file(): + continue + try: + contents.append(path.read_text(encoding="utf-8", errors="replace")) + except OSError: + raise click.UsageError( + f"Could not inspect dependency file for embedded credentials: {path}" + ) from None + _validate_git_http_url_userinfo(contents) + + +def _validate_local_dependency_files(config_path: pathlib.Path, config: Config) -> None: + """Validate dependency files copied into a non-uv Python image.""" + paths: list[pathlib.Path] = [] + for dependency in config["dependencies"]: + if not isinstance(dependency, str) or not dependency.startswith("."): + continue + root = (config_path.parent / dependency).resolve() + paths.extend( + root / name + for name in ("requirements.txt", "pyproject.toml", "setup.py", "setup.cfg") + ) + _validate_git_http_url_userinfo_files(paths) + + MIN_PYTHON_VERSION = "3.11" DEFAULT_PYTHON_VERSION = "3.11" @@ -424,17 +471,11 @@ def validate_config(config: Config) -> Config: ' "source": {"kind": "uv", "root": ".."}' ) - if any( - isinstance(dependency, str) and _has_git_http_url_userinfo(dependency) + _validate_git_http_url_userinfo( + dependency for dependency in config["dependencies"] - ): - raise click.UsageError( - "Git dependency URLs must not contain credentials or other URL " - "userinfo because generated Dockerfiles and image layers can retain " - "them. Use a credential-free Git URL and provide short-lived " - "credentials through your build environment's secret-backed Git " - "credential helper." - ) + if isinstance(dependency, str) + ) source = config.get("source") source_kind = _get_source_kind(config) @@ -1301,6 +1342,7 @@ def python_config_to_docker( api_version=api_version, build_tools_to_uninstall=build_tools_to_uninstall, ) + _validate_local_dependency_files(config_path, config) if pip_installer == "auto": if _image_supports_uv(base_image): pip_installer = "uv" diff --git a/libs/cli/langgraph_cli/uv_lock.py b/libs/cli/langgraph_cli/uv_lock.py index 1e33d16d767..7866a7233c8 100644 --- a/libs/cli/langgraph_cli/uv_lock.py +++ b/libs/cli/langgraph_cli/uv_lock.py @@ -880,6 +880,7 @@ def python_config_to_docker_uv_lock( _get_node_pm_install_cmd, _get_pip_cleanup_lines, _image_supports_uv, + _validate_git_http_url_userinfo_files, docker_tag, ) @@ -890,11 +891,20 @@ def python_config_to_docker_uv_lock( ) config_root = config_path.parent.resolve() + source_root = config["source"].get("root", ".") + project_root = (config_root / source_root).resolve() + _validate_git_http_url_userinfo_files( + [project_root / "pyproject.toml", project_root / "uv.lock"] + ) + install_cmd = "uv pip install --system" _, global_reqs_pip_install, pip_config_file_str = _build_python_install_commands( config, install_cmd ) plan = _plan_uv_lock_workspace(config_path, config) + _validate_git_http_url_userinfo_files( + package.pyproject_path for package in plan.install_order + ) _update_uv_lock_graph_paths(config_path, config, plan) for section, key in [ diff --git a/libs/cli/tests/unit_tests/test_config.py b/libs/cli/tests/unit_tests/test_config.py index 3d34f415dcd..1e0be96d814 100644 --- a/libs/cli/tests/unit_tests/test_config.py +++ b/libs/cli/tests/unit_tests/test_config.py @@ -299,6 +299,85 @@ def test_validate_config_allows_git_urls_without_http_userinfo(dependency: str): assert config["dependencies"] == [dependency] +def test_config_to_docker_rejects_git_http_url_userinfo_in_requirements( + tmp_path: pathlib.Path, +): + config_path = tmp_path / "langgraph.json" + config_path.write_text("{}\n") + (tmp_path / "agent.py").write_text("graph = object()\n") + (tmp_path / "requirements.txt").write_text( + "private @ git+https://secret-token@github.com/org/private.git\n" + ) + config = validate_config( + { + "python_version": "3.11", + "dependencies": ["."], + "graphs": {"agent": "./agent.py:graph"}, + } + ) + + with pytest.raises(click.UsageError) as exc_info: + config_to_docker( + config_path, + config, + base_image="langchain/langgraph-api:0.2.47", + ) + + message = str(exc_info.value) + assert "must not contain credentials or other URL userinfo" in message + assert "secret-token" not in message + + +@pytest.mark.parametrize("manifest", ["pyproject.toml", "uv.lock"]) +def test_config_to_docker_rejects_git_http_url_userinfo_in_uv_files( + tmp_path: pathlib.Path, manifest: str +): + config_path = tmp_path / "langgraph.json" + config_path.write_text("{}\n") + (tmp_path / "src").mkdir() + (tmp_path / "src" / "agent.py").write_text("graph = object()\n") + pyproject = textwrap.dedent( + """ + [project] + name = "agent" + version = "0.1.0" + dependencies = ["private"] + + [tool.uv.sources] + private = { git = "https://github.com/org/private.git" } + """ + ).strip() + uv_lock = "# uv lock file\n" + if manifest == "pyproject.toml": + pyproject = pyproject.replace( + "https://github.com", "https://secret-token@github.com" + ) + else: + uv_lock += ( + 'source = { git = "https://secret-token@github.com/org/private.git" }\n' + ) + (tmp_path / "pyproject.toml").write_text(pyproject + "\n") + (tmp_path / "uv.lock").write_text(uv_lock) + config = validate_config( + { + "python_version": "3.11", + "graphs": {"agent": "./src/agent.py:graph"}, + "source": {"kind": "uv"}, + } + ) + + with pytest.raises(click.UsageError) as exc_info: + config_to_docker( + config_path, + config, + base_image="langchain/langgraph-api:0.2.47", + ) + + message = str(exc_info.value) + assert "must not contain credentials or other URL userinfo" in message + assert "secret-token" not in message + + def test_validate_config_image_distro(): """Test validation of image_distro field.""" # Valid image_distro values should work From 48ecb3bc816853fa3cd22fbb65ce2e8973997b6b Mon Sep 17 00:00:00 2001 From: John Kennedy <65985482+jkennedyvz@users.noreply.github.com> Date: Tue, 11 Aug 2026 09:40:13 -0700 Subject: [PATCH 3/5] fix(cli): address Git dependency review feedback --- libs/cli/README.md | 2 +- libs/cli/langgraph_cli/config.py | 28 ++++++-- libs/cli/langgraph_cli/schemas.py | 3 +- libs/cli/schemas/schema.json | 6 +- libs/cli/schemas/schema.v0.json | 6 +- libs/cli/tests/unit_tests/test_config.py | 92 ++++++++++++++++++++++++ 6 files changed, 123 insertions(+), 14 deletions(-) diff --git a/libs/cli/README.md b/libs/cli/README.md index 82016c0b919..9f33b561ad9 100644 --- a/libs/cli/README.md +++ b/libs/cli/README.md @@ -103,7 +103,7 @@ The CLI uses a `langgraph.json` configuration file with these key settings: } ``` -Git dependencies must use credential-free URLs. The CLI rejects HTTP Git URLs with userinfo because generated Dockerfiles and image layers can retain embedded usernames or tokens. For private dependencies, provide short-lived credentials through your build environment's secret-backed Git credential helper. Do not store credentials in `langgraph.json`, requirement or lock files, or a `pip_config_file` copied into the image. +Git dependencies should use credential-free URLs. The CLI conservatively scans direct `langgraph.json` dependencies, common Python package files, uv project and lock files, and common Node.js package and lock files for HTTP Git URLs with userinfo. This check is not exhaustive: generated Docker builds can copy other files, including nested requirement or constraint files, into image layers without scanning them. For private dependencies, provide short-lived credentials through your build environment's secret-backed Git credential helper. Do not store credentials in copied files such as `langgraph.json` or `pip_config_file`. See the [full documentation](https://reference.langchain.com/python/langgraph-cli) for detailed configuration options. diff --git a/libs/cli/langgraph_cli/config.py b/libs/cli/langgraph_cli/config.py index aabc824d760..5fdfc6cfbfe 100644 --- a/libs/cli/langgraph_cli/config.py +++ b/libs/cli/langgraph_cli/config.py @@ -92,32 +92,37 @@ def _has_git_http_url_userinfo(dependency: str) -> bool: ) -def _validate_git_http_url_userinfo(values: Iterable[str]) -> None: +def _validate_git_http_url_userinfo( + values: Iterable[str], *, source: pathlib.Path | None = None +) -> None: """Reject credential-bearing Git HTTP URLs without echoing their values.""" if not any(_has_git_http_url_userinfo(value) for value in values): return - raise click.UsageError( + message = ( "Git dependency URLs must not contain credentials or other URL " "userinfo because generated Dockerfiles and image layers can retain " "them. Use a credential-free Git URL and provide short-lived " "credentials through your build environment's secret-backed Git " "credential helper." ) + if source is not None: + message += f" Found in: {source}" + raise click.UsageError(message) def _validate_git_http_url_userinfo_files(paths: Iterable[pathlib.Path]) -> None: """Reject credential-bearing Git HTTP URLs in dependency files.""" - contents: list[str] = [] for path in paths: + path = path.resolve() if not path.is_file(): continue try: - contents.append(path.read_text(encoding="utf-8", errors="replace")) + contents = path.read_text(encoding="utf-8", errors="replace") except OSError: raise click.UsageError( f"Could not inspect dependency file for embedded credentials: {path}" ) from None - _validate_git_http_url_userinfo(contents) + _validate_git_http_url_userinfo([contents], source=path) def _validate_local_dependency_files(config_path: pathlib.Path, config: Config) -> None: @@ -1553,7 +1558,18 @@ def node_config_to_docker( ) -> tuple[str, dict[str, str]]: # Calculate paths for monorepo support install_root = ( - pathlib.Path(build_context).resolve() if build_context else config_path.parent + pathlib.Path(build_context).resolve() + if build_context + else config_path.parent.resolve() + ) + config_root = config_path.parent.resolve() + dependency_roots = ( + (install_root, config_root) if install_root != config_root else (install_root,) + ) + _validate_git_http_url_userinfo_files( + root / name + for root in dependency_roots + for name in ("package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml") ) install_cmd = install_command or _get_node_pm_install_cmd(install_root) if build_context: diff --git a/libs/cli/langgraph_cli/schemas.py b/libs/cli/langgraph_cli/schemas.py index 6678d7d6761..1d527a94eea 100644 --- a/libs/cli/langgraph_cli/schemas.py +++ b/libs/cli/langgraph_cli/schemas.py @@ -650,7 +650,8 @@ class Config(TypedDict, total=False): pip_config_file: str | None """Optional. Path to a pip config file (e.g., "/etc/pip.conf" or "pip.ini") for controlling - package installation (custom indices, credentials, etc.). + package installation (custom indices, timeouts, etc.). The file is copied into the + generated image, so it must not contain credentials or other secrets. Only relevant if Python dependencies are installed via pip. If omitted, default pip settings are used. """ diff --git a/libs/cli/schemas/schema.json b/libs/cli/schemas/schema.json index 6eec6205b34..91dfad2f234 100644 --- a/libs/cli/schemas/schema.json +++ b/libs/cli/schemas/schema.json @@ -28,7 +28,7 @@ "type": "null" } ], - "description": "Optional. Path to a pip config file (e.g., \"/etc/pip.conf\" or \"pip.ini\") for controlling\npackage installation (custom indices, credentials, etc.).\n\nOnly relevant if Python dependencies are installed via pip. If omitted, default pip settings are used.\n" + "description": "Optional. Path to a pip config file (e.g., \"/etc/pip.conf\" or \"pip.ini\") for controlling\npackage installation (custom indices, timeouts, etc.). The file is copied into the\ngenerated image, so it must not contain credentials or other secrets.\n\nOnly relevant if Python dependencies are installed via pip. If omitted, default pip settings are used.\n" }, "_INTERNAL_docker_tag": { "anyOf": [ @@ -270,7 +270,7 @@ "type": "null" } ], - "description": "Optional. Path to a pip config file (e.g., \"/etc/pip.conf\" or \"pip.ini\") for controlling\npackage installation (custom indices, credentials, etc.).\n\nOnly relevant if Python dependencies are installed via pip. If omitted, default pip settings are used.\n" + "description": "Optional. Path to a pip config file (e.g., \"/etc/pip.conf\" or \"pip.ini\") for controlling\npackage installation (custom indices, timeouts, etc.). The file is copied into the\ngenerated image, so it must not contain credentials or other secrets.\n\nOnly relevant if Python dependencies are installed via pip. If omitted, default pip settings are used.\n" }, "_INTERNAL_docker_tag": { "anyOf": [ @@ -1346,4 +1346,4 @@ "title": "LangGraph CLI Configuration", "description": "Configuration schema for langgraph-cli", "version": "v0" -} \ No newline at end of file +} diff --git a/libs/cli/schemas/schema.v0.json b/libs/cli/schemas/schema.v0.json index 6eec6205b34..91dfad2f234 100644 --- a/libs/cli/schemas/schema.v0.json +++ b/libs/cli/schemas/schema.v0.json @@ -28,7 +28,7 @@ "type": "null" } ], - "description": "Optional. Path to a pip config file (e.g., \"/etc/pip.conf\" or \"pip.ini\") for controlling\npackage installation (custom indices, credentials, etc.).\n\nOnly relevant if Python dependencies are installed via pip. If omitted, default pip settings are used.\n" + "description": "Optional. Path to a pip config file (e.g., \"/etc/pip.conf\" or \"pip.ini\") for controlling\npackage installation (custom indices, timeouts, etc.). The file is copied into the\ngenerated image, so it must not contain credentials or other secrets.\n\nOnly relevant if Python dependencies are installed via pip. If omitted, default pip settings are used.\n" }, "_INTERNAL_docker_tag": { "anyOf": [ @@ -270,7 +270,7 @@ "type": "null" } ], - "description": "Optional. Path to a pip config file (e.g., \"/etc/pip.conf\" or \"pip.ini\") for controlling\npackage installation (custom indices, credentials, etc.).\n\nOnly relevant if Python dependencies are installed via pip. If omitted, default pip settings are used.\n" + "description": "Optional. Path to a pip config file (e.g., \"/etc/pip.conf\" or \"pip.ini\") for controlling\npackage installation (custom indices, timeouts, etc.). The file is copied into the\ngenerated image, so it must not contain credentials or other secrets.\n\nOnly relevant if Python dependencies are installed via pip. If omitted, default pip settings are used.\n" }, "_INTERNAL_docker_tag": { "anyOf": [ @@ -1346,4 +1346,4 @@ "title": "LangGraph CLI Configuration", "description": "Configuration schema for langgraph-cli", "version": "v0" -} \ No newline at end of file +} diff --git a/libs/cli/tests/unit_tests/test_config.py b/libs/cli/tests/unit_tests/test_config.py index 1e0be96d814..bcd8c8c1c28 100644 --- a/libs/cli/tests/unit_tests/test_config.py +++ b/libs/cli/tests/unit_tests/test_config.py @@ -261,6 +261,7 @@ def test_validate_config(): "git+https://user:secret-token@github.com/org/private.git@main", "private-package @ git+http://token@github.com/org/private.git", "git+HTTPS://user%40example.com:secret%2Ftoken@github.com/org/private.git", + "git+https://${GIT_TOKEN}@github.com/org/private.git", ], ) def test_validate_config_rejects_git_http_url_userinfo(dependency: str): @@ -279,6 +280,96 @@ def test_validate_config_rejects_git_http_url_userinfo(dependency: str): assert "secret%2Ftoken" not in message +@pytest.mark.parametrize( + "manifest", ["package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml"] +) +def test_config_to_docker_rejects_git_http_url_userinfo_in_node_files( + tmp_path: pathlib.Path, manifest: str +): + config_path = tmp_path / "langgraph.json" + config_path.write_text("{}\n") + (tmp_path / "agent.js").write_text("export const graph = {};\n") + (tmp_path / "package.json").write_text('{"name":"agent"}\n') + (tmp_path / manifest).write_text( + '"priv": "git+https://user:secret-token@github.com/org/private.git"\n' + ) + config = validate_config( + { + "node_version": "20", + "graphs": {"agent": "./agent.js:graph"}, + } + ) + + with pytest.raises(click.UsageError) as exc_info: + config_to_docker( + config_path, + config, + base_image="langchain/langgraphjs-api", + ) + + message = str(exc_info.value) + assert "must not contain credentials or other URL userinfo" in message + assert "secret-token" not in message + assert f"Found in: {(tmp_path / manifest).resolve()}" in message + + +def test_config_to_docker_allows_node_git_urls_without_http_userinfo( + tmp_path: pathlib.Path, +): + config_path = tmp_path / "langgraph.json" + config_path.write_text("{}\n") + (tmp_path / "agent.js").write_text("export const graph = {};\n") + (tmp_path / "package.json").write_text( + '{"dependencies":{"public":"git+https://github.com/org/public.git"}}\n' + ) + config = validate_config( + { + "node_version": "20", + "graphs": {"agent": "./agent.js:graph"}, + } + ) + + docker, _ = config_to_docker( + config_path, + config, + base_image="langchain/langgraphjs-api", + ) + + assert f"ADD . /deps/{tmp_path.name}" in docker + + +def test_config_to_docker_rejects_git_http_url_userinfo_in_node_workspace( + tmp_path: pathlib.Path, +): + config_root = tmp_path / "apps" / "agent" + config_root.mkdir(parents=True) + config_path = config_root / "langgraph.json" + config_path.write_text("{}\n") + (config_root / "agent.js").write_text("export const graph = {};\n") + (config_root / "package.json").write_text( + '{"dependencies":{"priv":"git+https://secret-token@github.com/org/private.git"}}\n' + ) + (tmp_path / "package.json").write_text('{"name":"workspace"}\n') + config = validate_config( + { + "node_version": "20", + "graphs": {"agent": "./agent.js:graph"}, + } + ) + + with pytest.raises(click.UsageError) as exc_info: + config_to_docker( + config_path, + config, + base_image="langchain/langgraphjs-api", + build_context=str(tmp_path), + ) + + message = str(exc_info.value) + assert "secret-token" not in message + assert f"Found in: {(config_root / 'package.json').resolve()}" in message + + @pytest.mark.parametrize( "dependency", [ @@ -326,6 +417,7 @@ def test_config_to_docker_rejects_git_http_url_userinfo_in_requirements( message = str(exc_info.value) assert "must not contain credentials or other URL userinfo" in message assert "secret-token" not in message + assert f"Found in: {(tmp_path / 'requirements.txt').resolve()}" in message @pytest.mark.parametrize("manifest", ["pyproject.toml", "uv.lock"]) From 62ecd5414eeb7305620003ca95f7eca6cd8863f7 Mon Sep 17 00:00:00 2001 From: John Kennedy <65985482+jkennedyvz@users.noreply.github.com> Date: Tue, 11 Aug 2026 10:00:27 -0700 Subject: [PATCH 4/5] fix(cli): stabilize generated schemas --- libs/cli/schemas/schema.json | 2 +- libs/cli/schemas/schema.v0.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/cli/schemas/schema.json b/libs/cli/schemas/schema.json index 91dfad2f234..a2d85de65a9 100644 --- a/libs/cli/schemas/schema.json +++ b/libs/cli/schemas/schema.json @@ -1346,4 +1346,4 @@ "title": "LangGraph CLI Configuration", "description": "Configuration schema for langgraph-cli", "version": "v0" -} +} \ No newline at end of file diff --git a/libs/cli/schemas/schema.v0.json b/libs/cli/schemas/schema.v0.json index 91dfad2f234..a2d85de65a9 100644 --- a/libs/cli/schemas/schema.v0.json +++ b/libs/cli/schemas/schema.v0.json @@ -1346,4 +1346,4 @@ "title": "LangGraph CLI Configuration", "description": "Configuration schema for langgraph-cli", "version": "v0" -} +} \ No newline at end of file From 0cce2d2f2beb5510be6337ad21679c3fd27ed405 Mon Sep 17 00:00:00 2001 From: John Kennedy <65985482+jkennedyvz@users.noreply.github.com> Date: Wed, 19 Aug 2026 23:57:21 +0000 Subject: [PATCH 5/5] fix(cli): report config source for invalid Git URLs Co-authored-by: open-swe[bot] --- libs/cli/langgraph_cli/config.py | 15 ++++++++++----- libs/cli/tests/unit_tests/test_config.py | 22 ++++++++++++++++++++++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/libs/cli/langgraph_cli/config.py b/libs/cli/langgraph_cli/config.py index 5fdfc6cfbfe..84c8425f920 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 bcd8c8c1c28..c691574585e 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"] )