langgraph-cli 0.4.32.dev0__py3-none-any.whl → 0.4.34__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- langgraph_cli/__init__.py +1 -1
- langgraph_cli/config.py +87 -3
- langgraph_cli/deploy.py +165 -18
- langgraph_cli/schemas.py +5 -1
- langgraph_cli/uv_lock.py +10 -0
- {langgraph_cli-0.4.32.dev0.dist-info → langgraph_cli-0.4.34.dist-info}/METADATA +3 -1
- {langgraph_cli-0.4.32.dev0.dist-info → langgraph_cli-0.4.34.dist-info}/RECORD +10 -10
- {langgraph_cli-0.4.32.dev0.dist-info → langgraph_cli-0.4.34.dist-info}/WHEEL +0 -0
- {langgraph_cli-0.4.32.dev0.dist-info → langgraph_cli-0.4.34.dist-info}/entry_points.txt +0 -0
- {langgraph_cli-0.4.32.dev0.dist-info → langgraph_cli-0.4.34.dist-info}/licenses/LICENSE +0 -0
langgraph_cli/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.4.
|
|
1
|
+
__version__ = "0.4.34"
|
langgraph_cli/config.py
CHANGED
|
@@ -6,6 +6,7 @@ import re
|
|
|
6
6
|
import shlex
|
|
7
7
|
import textwrap
|
|
8
8
|
from collections import Counter
|
|
9
|
+
from collections.abc import Iterable
|
|
9
10
|
from typing import Literal, NamedTuple
|
|
10
11
|
|
|
11
12
|
import click
|
|
@@ -36,6 +37,10 @@ DISALLOWED_BUILD_COMMAND_CHARS = [
|
|
|
36
37
|
# This blocks background execution (cmd &) while allowing command
|
|
37
38
|
# chaining (cmd1 && cmd2) which is common in build commands.
|
|
38
39
|
_SINGLE_AMPERSAND_RE = re.compile(r"(?<!&)&(?:&&)*(?!&)")
|
|
40
|
+
_GIT_HTTP_AUTHORITY_RES = (
|
|
41
|
+
re.compile(r"git\+https?://(?P<authority>[^/\s\"']+)", re.I),
|
|
42
|
+
re.compile(r"\bgit\s*=\s*[\"']https?://(?P<authority>[^/\s\"']+)", re.I),
|
|
43
|
+
)
|
|
39
44
|
_API_VERSION_PATTERN = re.compile(
|
|
40
45
|
r"^(?P<major>\d+)"
|
|
41
46
|
r"(?:\.(?P<minor>\d+))?"
|
|
@@ -78,6 +83,62 @@ def has_disallowed_build_command_content(command: str) -> bool:
|
|
|
78
83
|
return False
|
|
79
84
|
|
|
80
85
|
|
|
86
|
+
def _has_git_http_url_userinfo(dependency: str) -> bool:
|
|
87
|
+
"""Check whether a Git HTTP URL contains userinfo."""
|
|
88
|
+
return any(
|
|
89
|
+
"@" in match.group("authority")
|
|
90
|
+
for pattern in _GIT_HTTP_AUTHORITY_RES
|
|
91
|
+
for match in pattern.finditer(dependency)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _validate_git_http_url_userinfo(
|
|
96
|
+
values: Iterable[str], *, source: pathlib.Path | None = None
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Reject credential-bearing Git HTTP URLs without echoing their values."""
|
|
99
|
+
if not any(_has_git_http_url_userinfo(value) for value in values):
|
|
100
|
+
return
|
|
101
|
+
message = (
|
|
102
|
+
"Git dependency URLs must not contain credentials or other URL "
|
|
103
|
+
"userinfo because generated Dockerfiles and image layers can retain "
|
|
104
|
+
"them. Use a credential-free Git URL and provide short-lived "
|
|
105
|
+
"credentials through your build environment's secret-backed Git "
|
|
106
|
+
"credential helper."
|
|
107
|
+
)
|
|
108
|
+
if source is not None:
|
|
109
|
+
message += f" Found in: {source}"
|
|
110
|
+
raise click.UsageError(message)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _validate_git_http_url_userinfo_files(paths: Iterable[pathlib.Path]) -> None:
|
|
114
|
+
"""Reject credential-bearing Git HTTP URLs in dependency files."""
|
|
115
|
+
for path in paths:
|
|
116
|
+
path = path.resolve()
|
|
117
|
+
if not path.is_file():
|
|
118
|
+
continue
|
|
119
|
+
try:
|
|
120
|
+
contents = path.read_text(encoding="utf-8", errors="replace")
|
|
121
|
+
except OSError:
|
|
122
|
+
raise click.UsageError(
|
|
123
|
+
f"Could not inspect dependency file for embedded credentials: {path}"
|
|
124
|
+
) from None
|
|
125
|
+
_validate_git_http_url_userinfo([contents], source=path)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _validate_local_dependency_files(config_path: pathlib.Path, config: Config) -> None:
|
|
129
|
+
"""Validate dependency files copied into a non-uv Python image."""
|
|
130
|
+
paths: list[pathlib.Path] = []
|
|
131
|
+
for dependency in config["dependencies"]:
|
|
132
|
+
if not isinstance(dependency, str) or not dependency.startswith("."):
|
|
133
|
+
continue
|
|
134
|
+
root = (config_path.parent / dependency).resolve()
|
|
135
|
+
paths.extend(
|
|
136
|
+
root / name
|
|
137
|
+
for name in ("requirements.txt", "pyproject.toml", "setup.py", "setup.cfg")
|
|
138
|
+
)
|
|
139
|
+
_validate_git_http_url_userinfo_files(paths)
|
|
140
|
+
|
|
141
|
+
|
|
81
142
|
MIN_PYTHON_VERSION = "3.11"
|
|
82
143
|
DEFAULT_PYTHON_VERSION = "3.11"
|
|
83
144
|
|
|
@@ -320,7 +381,9 @@ def _get_source_kind(config: Config) -> str | None:
|
|
|
320
381
|
return kind if isinstance(kind, str) else None
|
|
321
382
|
|
|
322
383
|
|
|
323
|
-
def validate_config(
|
|
384
|
+
def validate_config(
|
|
385
|
+
config: Config, *, source_path: pathlib.Path | None = None
|
|
386
|
+
) -> Config:
|
|
324
387
|
"""Validate a configuration dictionary."""
|
|
325
388
|
|
|
326
389
|
graphs = config.get("graphs", {})
|
|
@@ -415,6 +478,15 @@ def validate_config(config: Config) -> Config:
|
|
|
415
478
|
' "source": {"kind": "uv", "root": ".."}'
|
|
416
479
|
)
|
|
417
480
|
|
|
481
|
+
_validate_git_http_url_userinfo(
|
|
482
|
+
(
|
|
483
|
+
dependency
|
|
484
|
+
for dependency in config["dependencies"]
|
|
485
|
+
if isinstance(dependency, str)
|
|
486
|
+
),
|
|
487
|
+
source=source_path,
|
|
488
|
+
)
|
|
489
|
+
|
|
418
490
|
source = config.get("source")
|
|
419
491
|
source_kind = _get_source_kind(config)
|
|
420
492
|
if source is not None and not isinstance(source, dict):
|
|
@@ -609,7 +681,7 @@ def validate_config_file(config_path: pathlib.Path) -> Config:
|
|
|
609
681
|
"""Load and validate a configuration file."""
|
|
610
682
|
with open(config_path) as f:
|
|
611
683
|
config = json.load(f)
|
|
612
|
-
validated = validate_config(config)
|
|
684
|
+
validated = validate_config(config, source_path=config_path.resolve())
|
|
613
685
|
# Enforce the package.json doesn't enforce an
|
|
614
686
|
# incompatible Node.js version
|
|
615
687
|
if validated.get("node_version"):
|
|
@@ -1280,6 +1352,7 @@ def python_config_to_docker(
|
|
|
1280
1352
|
api_version=api_version,
|
|
1281
1353
|
build_tools_to_uninstall=build_tools_to_uninstall,
|
|
1282
1354
|
)
|
|
1355
|
+
_validate_local_dependency_files(config_path, config)
|
|
1283
1356
|
if pip_installer == "auto":
|
|
1284
1357
|
if _image_supports_uv(base_image):
|
|
1285
1358
|
pip_installer = "uv"
|
|
@@ -1490,7 +1563,18 @@ def node_config_to_docker(
|
|
|
1490
1563
|
) -> tuple[str, dict[str, str]]:
|
|
1491
1564
|
# Calculate paths for monorepo support
|
|
1492
1565
|
install_root = (
|
|
1493
|
-
pathlib.Path(build_context).resolve()
|
|
1566
|
+
pathlib.Path(build_context).resolve()
|
|
1567
|
+
if build_context
|
|
1568
|
+
else config_path.parent.resolve()
|
|
1569
|
+
)
|
|
1570
|
+
config_root = config_path.parent.resolve()
|
|
1571
|
+
dependency_roots = (
|
|
1572
|
+
(install_root, config_root) if install_root != config_root else (install_root,)
|
|
1573
|
+
)
|
|
1574
|
+
_validate_git_http_url_userinfo_files(
|
|
1575
|
+
root / name
|
|
1576
|
+
for root in dependency_roots
|
|
1577
|
+
for name in ("package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml")
|
|
1494
1578
|
)
|
|
1495
1579
|
install_cmd = install_command or _get_node_pm_install_cmd(install_root)
|
|
1496
1580
|
if build_context:
|
langgraph_cli/deploy.py
CHANGED
|
@@ -32,7 +32,7 @@ from langgraph_cli.host_backend import (
|
|
|
32
32
|
HostBackendError,
|
|
33
33
|
SourceName,
|
|
34
34
|
)
|
|
35
|
-
from langgraph_cli.image_reference import ImageReference
|
|
35
|
+
from langgraph_cli.image_reference import DIGEST_SEPARATOR, ImageReference
|
|
36
36
|
from langgraph_cli.progress import Progress
|
|
37
37
|
from langgraph_cli.util import warn_non_wolfi_distro
|
|
38
38
|
|
|
@@ -624,6 +624,41 @@ def format_deployments_table(deployments: Sequence[dict[str, object]]) -> str:
|
|
|
624
624
|
return "\n".join(lines)
|
|
625
625
|
|
|
626
626
|
|
|
627
|
+
def _extract_listener_namespaces(listener: dict[str, object]) -> str:
|
|
628
|
+
compute_config = listener.get("compute_config")
|
|
629
|
+
namespaces = (
|
|
630
|
+
compute_config.get("k8s_namespaces")
|
|
631
|
+
if isinstance(compute_config, dict)
|
|
632
|
+
else None
|
|
633
|
+
)
|
|
634
|
+
if isinstance(namespaces, list) and namespaces:
|
|
635
|
+
return ", ".join(str(namespace) for namespace in namespaces)
|
|
636
|
+
return "-"
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def format_listeners_table(listeners: Sequence[dict[str, object]]) -> str:
|
|
640
|
+
headers = ("Listener ID", "Compute ID", "Namespaces")
|
|
641
|
+
rows = [
|
|
642
|
+
(
|
|
643
|
+
str(listener.get("id", "-") or "-"),
|
|
644
|
+
str(listener.get("compute_id", "-") or "-"),
|
|
645
|
+
_extract_listener_namespaces(listener),
|
|
646
|
+
)
|
|
647
|
+
for listener in listeners
|
|
648
|
+
]
|
|
649
|
+
widths = [
|
|
650
|
+
max(len(headers[index]), *(len(row[index]) for row in rows))
|
|
651
|
+
for index in range(len(headers))
|
|
652
|
+
]
|
|
653
|
+
|
|
654
|
+
def format_row(row: Sequence[str]) -> str:
|
|
655
|
+
return " ".join(value.ljust(widths[index]) for index, value in enumerate(row))
|
|
656
|
+
|
|
657
|
+
lines = [format_row(headers), format_row(tuple("-" * width for width in widths))]
|
|
658
|
+
lines.extend(format_row(row) for row in rows)
|
|
659
|
+
return "\n".join(lines)
|
|
660
|
+
|
|
661
|
+
|
|
627
662
|
def format_revisions_table(revisions: Sequence[dict[str, object]]) -> str:
|
|
628
663
|
headers = ("Revision ID", "Status", "Created At")
|
|
629
664
|
latest_deployed_seen = False
|
|
@@ -1544,9 +1579,9 @@ def _ensure_customer_registry_source(existing: ExistingDeployment) -> None:
|
|
|
1544
1579
|
if existing.source != _CUSTOMER_REGISTRY_SOURCE:
|
|
1545
1580
|
raise click.UsageError(
|
|
1546
1581
|
f"Deployment {existing.id} was not created from an external image "
|
|
1547
|
-
"and cannot be updated with --push-to
|
|
1548
|
-
"its current build mode, or use a different
|
|
1549
|
-
"deployment."
|
|
1582
|
+
"and cannot be updated with --push-to or --image-uri. Run without "
|
|
1583
|
+
"either flag to keep its current build mode, or use a different "
|
|
1584
|
+
"--name to create a new deployment."
|
|
1550
1585
|
)
|
|
1551
1586
|
|
|
1552
1587
|
|
|
@@ -1599,9 +1634,22 @@ class RemoteBuildSource:
|
|
|
1599
1634
|
|
|
1600
1635
|
|
|
1601
1636
|
@dataclass(frozen=True, slots=True)
|
|
1602
|
-
class
|
|
1637
|
+
class BuildAndPush:
|
|
1603
1638
|
reference: ImageReference
|
|
1604
1639
|
prebuilt_image: str | None
|
|
1640
|
+
|
|
1641
|
+
|
|
1642
|
+
@dataclass(frozen=True, slots=True)
|
|
1643
|
+
class PublishedImage:
|
|
1644
|
+
image_uri: str
|
|
1645
|
+
|
|
1646
|
+
|
|
1647
|
+
ImageSource = BuildAndPush | PublishedImage
|
|
1648
|
+
|
|
1649
|
+
|
|
1650
|
+
@dataclass(frozen=True, slots=True)
|
|
1651
|
+
class CustomerRegistrySource:
|
|
1652
|
+
image: ImageSource
|
|
1605
1653
|
requested_placement: RequestedPlacement
|
|
1606
1654
|
|
|
1607
1655
|
def run(self, ctx: DeployContext) -> DeployOutcome:
|
|
@@ -1686,16 +1734,22 @@ class CustomerRegistrySource:
|
|
|
1686
1734
|
)
|
|
1687
1735
|
|
|
1688
1736
|
def _publish(self, ctx: DeployContext, step: int) -> tuple[str, int]:
|
|
1689
|
-
|
|
1737
|
+
if isinstance(self.image, PublishedImage):
|
|
1738
|
+
return self.image.image_uri, step
|
|
1739
|
+
image = str(self.image.reference)
|
|
1690
1740
|
with Runner() as runner:
|
|
1691
|
-
if self.prebuilt_image:
|
|
1692
|
-
_log_deploy_step(step, f"Validating image {self.prebuilt_image}")
|
|
1741
|
+
if self.image.prebuilt_image:
|
|
1742
|
+
_log_deploy_step(step, f"Validating image {self.image.prebuilt_image}")
|
|
1693
1743
|
_validate_prebuilt_image(
|
|
1694
|
-
runner, self.prebuilt_image, verbose=ctx.verbose
|
|
1744
|
+
runner, self.image.prebuilt_image, verbose=ctx.verbose
|
|
1695
1745
|
)
|
|
1696
1746
|
runner.run(
|
|
1697
1747
|
subp_exec(
|
|
1698
|
-
"docker",
|
|
1748
|
+
"docker",
|
|
1749
|
+
"tag",
|
|
1750
|
+
self.image.prebuilt_image,
|
|
1751
|
+
image,
|
|
1752
|
+
verbose=ctx.verbose,
|
|
1699
1753
|
)
|
|
1700
1754
|
)
|
|
1701
1755
|
else:
|
|
@@ -1733,20 +1787,35 @@ def _push_reference(push_to: str, tag: str | None) -> ImageReference:
|
|
|
1733
1787
|
return reference.with_tag(normalize_image_tag(tag or _DEFAULT_IMAGE_TAG))
|
|
1734
1788
|
|
|
1735
1789
|
|
|
1790
|
+
def _validate_image_uri(image_uri: str) -> str:
|
|
1791
|
+
value = image_uri.strip()
|
|
1792
|
+
if not value:
|
|
1793
|
+
raise click.UsageError("--image-uri must not be empty.")
|
|
1794
|
+
if DIGEST_SEPARATOR not in value:
|
|
1795
|
+
raise click.UsageError(
|
|
1796
|
+
"--image-uri must pin a digest, e.g. "
|
|
1797
|
+
f"repository{DIGEST_SEPARATOR}<sha256 hex>. Kubernetes can cache "
|
|
1798
|
+
"images by tag, so redeploying a mutable tag may silently keep "
|
|
1799
|
+
"running the previous image."
|
|
1800
|
+
)
|
|
1801
|
+
return value
|
|
1802
|
+
|
|
1803
|
+
|
|
1736
1804
|
def _select_source(
|
|
1737
1805
|
*,
|
|
1738
1806
|
push_to: str | None,
|
|
1739
1807
|
image: str | None,
|
|
1808
|
+
image_uri: str | None,
|
|
1740
1809
|
image_name: str | None,
|
|
1741
1810
|
tag: str | None,
|
|
1742
1811
|
remote_build_flag: bool | None,
|
|
1743
1812
|
placement: RequestedPlacement,
|
|
1744
1813
|
selector: DeploymentSelector,
|
|
1745
1814
|
) -> DeploymentSource:
|
|
1746
|
-
if push_to is None and placement.requested:
|
|
1815
|
+
if push_to is None and image_uri is None and placement.requested:
|
|
1747
1816
|
raise click.UsageError(
|
|
1748
1817
|
"--listener-id and --k8s-namespace only apply when creating a "
|
|
1749
|
-
"deployment with --push-to."
|
|
1818
|
+
"deployment with --push-to or --image-uri."
|
|
1750
1819
|
)
|
|
1751
1820
|
if placement.requested and isinstance(selector, ById):
|
|
1752
1821
|
raise click.UsageError(
|
|
@@ -1754,6 +1823,19 @@ def _select_source(
|
|
|
1754
1823
|
"they cannot be set for an existing --deployment-id. Drop them, or "
|
|
1755
1824
|
"create a new deployment with --name."
|
|
1756
1825
|
)
|
|
1826
|
+
if image_uri is not None:
|
|
1827
|
+
if push_to is not None:
|
|
1828
|
+
raise click.UsageError("--image-uri cannot be combined with --push-to.")
|
|
1829
|
+
if image is not None:
|
|
1830
|
+
raise click.UsageError("--image-uri cannot be combined with --image.")
|
|
1831
|
+
if tag is not None:
|
|
1832
|
+
raise click.UsageError("--image-uri cannot be combined with --tag.")
|
|
1833
|
+
if remote_build_flag is not None:
|
|
1834
|
+
raise click.UsageError("--image-uri cannot be combined with --remote.")
|
|
1835
|
+
return CustomerRegistrySource(
|
|
1836
|
+
image=PublishedImage(_validate_image_uri(image_uri)),
|
|
1837
|
+
requested_placement=placement,
|
|
1838
|
+
)
|
|
1757
1839
|
if push_to is not None:
|
|
1758
1840
|
if remote_build_flag is True:
|
|
1759
1841
|
raise click.UsageError("--push-to cannot be combined with --remote.")
|
|
@@ -1761,8 +1843,7 @@ def _select_source(
|
|
|
1761
1843
|
if image is None:
|
|
1762
1844
|
_require_local_docker()
|
|
1763
1845
|
return CustomerRegistrySource(
|
|
1764
|
-
reference=reference,
|
|
1765
|
-
prebuilt_image=image,
|
|
1846
|
+
image=BuildAndPush(reference=reference, prebuilt_image=image),
|
|
1766
1847
|
requested_placement=placement,
|
|
1767
1848
|
)
|
|
1768
1849
|
if image and remote_build_flag is True:
|
|
@@ -1793,7 +1874,10 @@ def _create_host_backend_client(
|
|
|
1793
1874
|
host_url: str | None,
|
|
1794
1875
|
api_key: str | None,
|
|
1795
1876
|
env_vars: dict[str, str] | None = None,
|
|
1877
|
+
*,
|
|
1878
|
+
no_input: bool | None = None,
|
|
1796
1879
|
) -> HostBackendClient:
|
|
1880
|
+
effective_no_input = _no_input if no_input is None else no_input
|
|
1797
1881
|
if env_vars is None:
|
|
1798
1882
|
env_vars = _parse_env_from_config({}, pathlib.Path.cwd() / DEFAULT_CONFIG)
|
|
1799
1883
|
resolved_api_key = api_key
|
|
@@ -1808,7 +1892,7 @@ def _create_host_backend_client(
|
|
|
1808
1892
|
resolved_api_key = val
|
|
1809
1893
|
break
|
|
1810
1894
|
if not resolved_api_key:
|
|
1811
|
-
if
|
|
1895
|
+
if effective_no_input:
|
|
1812
1896
|
raise click.ClickException(
|
|
1813
1897
|
"No LangSmith API key found. Set LANGSMITH_API_KEY in the "
|
|
1814
1898
|
"environment or .env file."
|
|
@@ -1833,6 +1917,8 @@ def _create_host_backend_client(
|
|
|
1833
1917
|
def _call_host_backend_with_optional_tenant(
|
|
1834
1918
|
client: HostBackendClient,
|
|
1835
1919
|
operation: Callable[[HostBackendClient], _T],
|
|
1920
|
+
*,
|
|
1921
|
+
no_input: bool | None = None,
|
|
1836
1922
|
) -> _T:
|
|
1837
1923
|
"""Run *operation*, prompting for a workspace ID on org-scoped 403s.
|
|
1838
1924
|
|
|
@@ -1841,6 +1927,7 @@ def _call_host_backend_with_optional_tenant(
|
|
|
1841
1927
|
in-place so all subsequent calls through the same instance are
|
|
1842
1928
|
tenant-aware.
|
|
1843
1929
|
"""
|
|
1930
|
+
effective_no_input = _no_input if no_input is None else no_input
|
|
1844
1931
|
prompted_for_tenant = False
|
|
1845
1932
|
|
|
1846
1933
|
while True:
|
|
@@ -1852,7 +1939,7 @@ def _call_host_backend_with_optional_tenant(
|
|
|
1852
1939
|
and err.status_code == 403
|
|
1853
1940
|
and "requires workspace specification" in err.message
|
|
1854
1941
|
):
|
|
1855
|
-
if
|
|
1942
|
+
if effective_no_input:
|
|
1856
1943
|
raise click.ClickException(
|
|
1857
1944
|
"API key is org-scoped and requires a workspace ID. "
|
|
1858
1945
|
"Set LANGSMITH_TENANT_ID in your .env file or "
|
|
@@ -2075,19 +2162,30 @@ def _deploy_base_options(
|
|
|
2075
2162
|
"Give the tag here or with --tag (default: latest)."
|
|
2076
2163
|
),
|
|
2077
2164
|
),
|
|
2165
|
+
click.option(
|
|
2166
|
+
"--image-uri",
|
|
2167
|
+
help=(
|
|
2168
|
+
"Deploy an image that's already in a registry you manage, "
|
|
2169
|
+
"without building, retagging, or pushing anything. For "
|
|
2170
|
+
"self-hosted and hybrid LangSmith. Give the full reference, "
|
|
2171
|
+
"e.g. 123456789.dkr.ecr.us-east-1.amazonaws.com/agents/"
|
|
2172
|
+
"my-agent:v1.2.3 or ...@sha256:<digest>. Cannot be combined "
|
|
2173
|
+
"with --push-to, --image, --tag, or --remote."
|
|
2174
|
+
),
|
|
2175
|
+
),
|
|
2078
2176
|
click.option(
|
|
2079
2177
|
"--listener-id",
|
|
2080
2178
|
help=(
|
|
2081
2179
|
"Listener that will run the deployment, for workspaces that "
|
|
2082
2180
|
"deploy through a listener in your own cluster. Only used when "
|
|
2083
|
-
"creating a deployment with --push-to."
|
|
2181
|
+
"creating a deployment with --push-to or --image-uri."
|
|
2084
2182
|
),
|
|
2085
2183
|
),
|
|
2086
2184
|
click.option(
|
|
2087
2185
|
"--k8s-namespace",
|
|
2088
2186
|
help=(
|
|
2089
2187
|
"Kubernetes namespace the listener deploys into. Only used when "
|
|
2090
|
-
"creating a deployment with --push-to."
|
|
2188
|
+
"creating a deployment with --push-to or --image-uri."
|
|
2091
2189
|
),
|
|
2092
2190
|
),
|
|
2093
2191
|
click.option(
|
|
@@ -2200,6 +2298,7 @@ def _deploy_cmd(
|
|
|
2200
2298
|
image_name: str | None,
|
|
2201
2299
|
image: str | None,
|
|
2202
2300
|
push_to: str | None,
|
|
2301
|
+
image_uri: str | None,
|
|
2203
2302
|
listener_id: str | None,
|
|
2204
2303
|
k8s_namespace: str | None,
|
|
2205
2304
|
tag: str | None,
|
|
@@ -2273,6 +2372,7 @@ def _deploy_cmd(
|
|
|
2273
2372
|
source = _select_source(
|
|
2274
2373
|
push_to=push_to,
|
|
2275
2374
|
image=image,
|
|
2375
|
+
image_uri=image_uri,
|
|
2276
2376
|
image_name=image_name,
|
|
2277
2377
|
tag=tag,
|
|
2278
2378
|
remote_build_flag=remote_build_flag,
|
|
@@ -2408,6 +2508,53 @@ def deploy_list(
|
|
|
2408
2508
|
click.echo(format_deployments_table(deployments))
|
|
2409
2509
|
|
|
2410
2510
|
|
|
2511
|
+
# ---------------------------------------------------------------------------
|
|
2512
|
+
# deploy listeners
|
|
2513
|
+
# ---------------------------------------------------------------------------
|
|
2514
|
+
|
|
2515
|
+
|
|
2516
|
+
@deploy.group(
|
|
2517
|
+
"listeners",
|
|
2518
|
+
cls=NestedHelpGroup,
|
|
2519
|
+
help="[Beta] Inspect listeners available to this workspace.",
|
|
2520
|
+
)
|
|
2521
|
+
def deploy_listeners() -> None:
|
|
2522
|
+
pass
|
|
2523
|
+
|
|
2524
|
+
|
|
2525
|
+
@OPT_HOST_API_KEY
|
|
2526
|
+
@OPT_HOST_URL
|
|
2527
|
+
@click.option(
|
|
2528
|
+
"--json",
|
|
2529
|
+
"json_output",
|
|
2530
|
+
is_flag=True,
|
|
2531
|
+
default=False,
|
|
2532
|
+
help="Emit the listeners as a JSON array instead of a table.",
|
|
2533
|
+
)
|
|
2534
|
+
@deploy_listeners.command(
|
|
2535
|
+
"list",
|
|
2536
|
+
help=(
|
|
2537
|
+
"[Beta] List listeners available to this workspace.\n\n"
|
|
2538
|
+
"Pass a listener's id to `langgraph deploy --push-to ... "
|
|
2539
|
+
"--listener-id <id>` to deploy through it."
|
|
2540
|
+
),
|
|
2541
|
+
)
|
|
2542
|
+
def deploy_listeners_list(
|
|
2543
|
+
api_key: str | None, host_url: str | None, json_output: bool
|
|
2544
|
+
) -> None:
|
|
2545
|
+
client = _create_host_backend_client(host_url, api_key, no_input=json_output)
|
|
2546
|
+
listeners = _call_host_backend_with_optional_tenant(
|
|
2547
|
+
client, lambda c: c.list_listeners(), no_input=json_output
|
|
2548
|
+
)
|
|
2549
|
+
if json_output:
|
|
2550
|
+
click.echo(json_mod.dumps(listeners))
|
|
2551
|
+
return
|
|
2552
|
+
if not listeners:
|
|
2553
|
+
click.echo("No listeners found for this workspace.")
|
|
2554
|
+
return
|
|
2555
|
+
click.echo(format_listeners_table(listeners))
|
|
2556
|
+
|
|
2557
|
+
|
|
2411
2558
|
# ---------------------------------------------------------------------------
|
|
2412
2559
|
# deploy revisions
|
|
2413
2560
|
# ---------------------------------------------------------------------------
|
langgraph_cli/schemas.py
CHANGED
|
@@ -650,7 +650,8 @@ class Config(TypedDict, total=False):
|
|
|
650
650
|
|
|
651
651
|
pip_config_file: str | None
|
|
652
652
|
"""Optional. Path to a pip config file (e.g., "/etc/pip.conf" or "pip.ini") for controlling
|
|
653
|
-
package installation (custom indices,
|
|
653
|
+
package installation (custom indices, timeouts, etc.). The file is copied into the
|
|
654
|
+
generated image, so it must not contain credentials or other secrets.
|
|
654
655
|
|
|
655
656
|
Only relevant if Python dependencies are installed via pip. If omitted, default pip settings are used.
|
|
656
657
|
"""
|
|
@@ -689,6 +690,9 @@ class Config(TypedDict, total=False):
|
|
|
689
690
|
- "." or "./src" if you have a local Python package
|
|
690
691
|
- str (aka "anthropic") for a PyPI package
|
|
691
692
|
- "git+https://github.com/org/repo.git@main" for a Git-based package
|
|
693
|
+
Git HTTP URLs must not contain userinfo such as a username or token. For private
|
|
694
|
+
dependencies, provide short-lived credentials through the build environment's
|
|
695
|
+
secret-backed Git credential helper.
|
|
692
696
|
Defaults to an empty list, meaning no additional packages installed beyond your base environment.
|
|
693
697
|
|
|
694
698
|
This field is not supported when `source.kind` is `uv`.
|
langgraph_cli/uv_lock.py
CHANGED
|
@@ -880,6 +880,7 @@ def python_config_to_docker_uv_lock(
|
|
|
880
880
|
_get_node_pm_install_cmd,
|
|
881
881
|
_get_pip_cleanup_lines,
|
|
882
882
|
_image_supports_uv,
|
|
883
|
+
_validate_git_http_url_userinfo_files,
|
|
883
884
|
docker_tag,
|
|
884
885
|
)
|
|
885
886
|
|
|
@@ -890,11 +891,20 @@ def python_config_to_docker_uv_lock(
|
|
|
890
891
|
)
|
|
891
892
|
|
|
892
893
|
config_root = config_path.parent.resolve()
|
|
894
|
+
source_root = config["source"].get("root", ".")
|
|
895
|
+
project_root = (config_root / source_root).resolve()
|
|
896
|
+
_validate_git_http_url_userinfo_files(
|
|
897
|
+
[project_root / "pyproject.toml", project_root / "uv.lock"]
|
|
898
|
+
)
|
|
899
|
+
|
|
893
900
|
install_cmd = "uv pip install --system"
|
|
894
901
|
_, global_reqs_pip_install, pip_config_file_str = _build_python_install_commands(
|
|
895
902
|
config, install_cmd
|
|
896
903
|
)
|
|
897
904
|
plan = _plan_uv_lock_workspace(config_path, config)
|
|
905
|
+
_validate_git_http_url_userinfo_files(
|
|
906
|
+
package.pyproject_path for package in plan.install_order
|
|
907
|
+
)
|
|
898
908
|
|
|
899
909
|
_update_uv_lock_graph_paths(config_path, config, plan)
|
|
900
910
|
for section, key in [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: langgraph-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.34
|
|
4
4
|
Summary: CLI for interacting with LangGraph API
|
|
5
5
|
Project-URL: Source, https://github.com/langchain-ai/langgraph/tree/main/libs/cli
|
|
6
6
|
Project-URL: Twitter, https://x.com/langchain_oss
|
|
@@ -125,6 +125,8 @@ The CLI uses a `langgraph.json` configuration file with these key settings:
|
|
|
125
125
|
}
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
+
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`.
|
|
129
|
+
|
|
128
130
|
See the [full documentation](https://reference.langchain.com/python/langgraph-cli) for detailed configuration options.
|
|
129
131
|
|
|
130
132
|
## Development
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
langgraph_cli/__init__.py,sha256=
|
|
1
|
+
langgraph_cli/__init__.py,sha256=lEcP5iYKQN3Lr-tUbNBN0qjKzIg4RjGwnIQfJt42ids,23
|
|
2
2
|
langgraph_cli/__main__.py,sha256=8hDtWlaFZK24KhfNq_ZKgtXqYHsDQDetukOCMlsbW0Q,59
|
|
3
3
|
langgraph_cli/_ignore.py,sha256=lh1JyfPtl0CbpCGtQuZ0ba2Cedsbblc2qLp-dmqdaRs,4193
|
|
4
4
|
langgraph_cli/analytics.py,sha256=z0KGKtSToqR7Q9aFoVOPAoQt6kIQsiPDk1N9i8wkT98,2706
|
|
5
5
|
langgraph_cli/archive.py,sha256=wmf5yXGU17Z8w5ZfIbRquXCaZ8W9OEH_BVfsJjGnZDI,4808
|
|
6
6
|
langgraph_cli/cli.py,sha256=Q4kpglojg3rkRVFIuodIUrtxoQyDv6ZKzXAV_k5UONg,34316
|
|
7
|
-
langgraph_cli/config.py,sha256=
|
|
7
|
+
langgraph_cli/config.py,sha256=pK-du8Qb9g0Dn0ojo8Rku_kB10MPtJJ1U87QJTsCr9g,69880
|
|
8
8
|
langgraph_cli/constants.py,sha256=iSE-4HZJRd4CXzOmPZPKgPLWDDw1v__5s3AnwZtMMIg,362
|
|
9
9
|
langgraph_cli/dependency_tracking.py,sha256=VaE5taa7qWP4McHhKJATcAXEC1XPPDrcPdj_gg9a8V0,4544
|
|
10
|
-
langgraph_cli/deploy.py,sha256=
|
|
10
|
+
langgraph_cli/deploy.py,sha256=vdGJiCTCgzLM43GJA1wpsroitOQ3z1dskMFQqwYpTGM,97137
|
|
11
11
|
langgraph_cli/docker.py,sha256=rVeC60lXpXQ9AoGn50LfW6bfS0bFvzWGXo_4SO2HW_8,12888
|
|
12
12
|
langgraph_cli/exec.py,sha256=vDtFhf2C_5jTTOZbV6cV1z2ioa947uBQvLj80eYXWH0,5343
|
|
13
13
|
langgraph_cli/host_backend.py,sha256=vyeYZxWG46xwBTXel2a7mCg3EExgzftf-PetO9Jkl-E,12134
|
|
14
14
|
langgraph_cli/image_reference.py,sha256=xX2VZ6tk9RRk-QjbWnDD38kIt-jTVFAMxB0pkYkxW-g,1107
|
|
15
15
|
langgraph_cli/progress.py,sha256=YBWJ2TAScNt2Jv8sd3suWu-QkySPvQb4q7HFg9iJ3wU,3412
|
|
16
16
|
langgraph_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
-
langgraph_cli/schemas.py,sha256=
|
|
17
|
+
langgraph_cli/schemas.py,sha256=sf2Z-X53KpuBSqHW6jUKm94OVIEn_iDcba2vqx2Ndkg,27590
|
|
18
18
|
langgraph_cli/templates.py,sha256=SPu0R1DIYOiNb8JxhKJX78zpakHiqoW9ddZRqRGRcu4,6978
|
|
19
19
|
langgraph_cli/util.py,sha256=tDJBts8zF-F15icCsRjX2OfpjKBF-CAADj2Z8qW-BuU,1619
|
|
20
|
-
langgraph_cli/uv_lock.py,sha256=
|
|
20
|
+
langgraph_cli/uv_lock.py,sha256=t3CnL2rY5t6ThfGidqdY046O_FkbzHFwsmbvnqIWWN0,38909
|
|
21
21
|
langgraph_cli/version.py,sha256=7oakgfTwsJJz0D5Sso_XKXkUzfLdN3fyVwgMTncms-A,308
|
|
22
|
-
langgraph_cli-0.4.
|
|
23
|
-
langgraph_cli-0.4.
|
|
24
|
-
langgraph_cli-0.4.
|
|
25
|
-
langgraph_cli-0.4.
|
|
26
|
-
langgraph_cli-0.4.
|
|
22
|
+
langgraph_cli-0.4.34.dist-info/METADATA,sha256=t4JaKnc0pSUdtcxjVexJSOjEkrG4HmlwTYhjaQTIg-s,5787
|
|
23
|
+
langgraph_cli-0.4.34.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
24
|
+
langgraph_cli-0.4.34.dist-info/entry_points.txt,sha256=qNTLVRQfXrOI4SWPyBueUpAu_6-sZFNmj1Xkum0o0yY,52
|
|
25
|
+
langgraph_cli-0.4.34.dist-info/licenses/LICENSE,sha256=2btS8uNUDWD_UNjw9ba6ZJt_00aUjEw9CGyK-xIHY8c,1072
|
|
26
|
+
langgraph_cli-0.4.34.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|