Compare commits

...
Author SHA1 Message Date
William FHandGitHub 2f1a16006a release(cli): 0.4.8 (#6556)
For webhook configuration support
2025-12-09 13:25:02 +00:00
William FHandGitHub 269d08f5d3 feat(cli): webhook configuration (#6555) 2025-12-09 13:17:22 +00:00
10 changed files with 842 additions and 422 deletions
+2 -2
View File
@@ -116,13 +116,13 @@ jobs:
strategy:
matrix:
python-version:
- "3.11"
- "3.13"
steps:
- uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: astral-sh/setup-uv@v7
with:
python-version: "3.11"
python-version: "3.13"
enable-cache: true
cache-suffix: "schema-check-cli"
- name: Install CLI dependencies
+4 -1
View File
@@ -1,4 +1,4 @@
.PHONY: test lint format test-integration update-schema
.PHONY: test lint format test-integration update-schema bump-version
######################
# TESTING AND COVERAGE
@@ -35,3 +35,6 @@ format format_diff:
update-schema:
uv run python generate_schema.py
bump-version:
uv run hatch version patch
+4
View File
@@ -27,6 +27,8 @@ from langgraph_cli.schemas import (
StoreConfig,
ThreadTTLConfig,
TTLConfig,
WebhooksConfig,
WebhookUrlPolicy,
)
@@ -118,6 +120,8 @@ def add_descriptions_to_schema(schema, cls):
SerdeConfig,
TTLConfig,
ConfigurableHeaderConfig,
WebhooksConfig,
WebhookUrlPolicy,
]:
if potential_cls.__name__ == def_name:
add_descriptions_to_schema(def_schema, potential_cls)
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.4.7"
__version__ = "0.4.8"
+10
View File
@@ -156,6 +156,8 @@ def validate_config(config: Config) -> Config:
"auth": config.get("auth"),
"encryption": config.get("encryption"),
"http": config.get("http"),
# Pass through webhooks config so it can be injected into the image
"webhooks": config.get("webhooks"),
"checkpointer": config.get("checkpointer"),
"ui": config.get("ui"),
"ui_config": config.get("ui_config"),
@@ -959,6 +961,10 @@ ADD {relpath} /deps/{name}
if (http_config := config.get("http")) is not None:
env_vars.append(f"ENV LANGGRAPH_HTTP='{json.dumps(http_config)}'")
# Inject webhooks configuration if provided
if (webhooks_config := config.get("webhooks")) is not None:
env_vars.append(f"ENV LANGGRAPH_WEBHOOKS='{json.dumps(webhooks_config)}'")
if (checkpointer_config := config.get("checkpointer")) is not None:
env_vars.append(
f"ENV LANGGRAPH_CHECKPOINTER='{json.dumps(checkpointer_config)}'"
@@ -1085,6 +1091,10 @@ def node_config_to_docker(
if (http_config := config.get("http")) is not None:
env_vars.append(f"ENV LANGGRAPH_HTTP='{json.dumps(http_config)}'")
# Inject webhooks configuration if provided
if (webhooks_config := config.get("webhooks")) is not None:
env_vars.append(f"ENV LANGGRAPH_WEBHOOKS='{json.dumps(webhooks_config)}'")
if (checkpointer_config := config.get("checkpointer")) is not None:
env_vars.append(
f"ENV LANGGRAPH_CHECKPOINTER='{json.dumps(checkpointer_config)}'"
+49 -2
View File
@@ -362,7 +362,7 @@ class CorsConfig(TypedDict, total=False):
"""
class ConfigurableHeaderConfig(TypedDict):
class ConfigurableHeaderConfig(TypedDict, total=False):
"""Customize which headers to include as configurable values in your runs.
By default, omits x-api-key, x-tenant-id, and x-service-key.
@@ -373,7 +373,7 @@ class ConfigurableHeaderConfig(TypedDict):
"""
includes: list[str] | None
"""Headers to include (if not also matches against an 'exludes' pattern.
"""Headers to include (if not also matched against an 'excludes' pattern).
Examples:
- 'user-agent'
@@ -485,6 +485,46 @@ class HttpConfig(TypedDict, total=False):
"""
class WebhookUrlPolicy(TypedDict, total=False):
require_https: bool
"""Enforce HTTPS scheme for absolute URLs; reject `http://` when true."""
allowed_domains: list[str]
"""Hostname allowlist. Supports exact hosts and wildcard subdomains.
Use entries like "hooks.example.com" or "*.mycorp.com". The wildcard only
matches subdomains ("foo.mycorp.com"), not the apex ("mycorp.com"). When
empty or omitted, any public host is allowed (subject to SSRF IP checks).
"""
allowed_ports: list[int]
"""Explicit port allowlist for absolute URLs.
If set, requests must use one of these ports. Defaults are respected when
a port is not present in the URL (443 for https, 80 for http).
"""
max_url_length: int
"""Maximum permitted URL length in characters; longer inputs are rejected early."""
disable_loopback: bool
"""Disallow relative URLs (internal loopback calls) when true."""
class WebhooksConfig(TypedDict, total=False):
env_prefix: str
"""Required prefix for environment variables referenced in header templates.
Acts as an allowlist boundary to prevent leaking arbitrary environment
variables. Defaults to "LG_WEBHOOK_" when omitted.
"""
url: WebhookUrlPolicy
"""URL validation policy for user-supplied webhook endpoints."""
headers: dict[str, str]
"""Static headers to include with webhook requests.
Values may contain templates of the form "${{ env.VAR }}". On startup, these
are resolved via the process environment after verifying `VAR` starts with
`env_prefix`. Mixed literals and multiple templates are allowed.
"""
class Config(TypedDict, total=False):
"""Top-level config for langgraph-cli or similar deployment tooling."""
@@ -613,6 +653,13 @@ class Config(TypedDict, total=False):
and how cross-origin requests are handled.
"""
webhooks: WebhooksConfig | None
"""Optional. Webhooks configuration for outbound event delivery.
Forwarded into the container as `LANGGRAPH_WEBHOOKS`. See `WebhooksConfig`
for URL policy and header templating details.
"""
ui: dict[str, str] | None
"""Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.
"""
+82 -6
View File
@@ -210,6 +210,17 @@
}
],
"description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
},
"webhooks": {
"anyOf": [
{
"$ref": "#/$defs/WebhooksConfig"
},
{
"type": "null"
}
],
"description": "Optional. Webhooks configuration for outbound event delivery.\n\nForwarded into the container as `LANGGRAPH_WEBHOOKS`. See `WebhooksConfig`\nfor URL policy and header templating details.\n"
}
},
"required": [
@@ -413,6 +424,17 @@
}
],
"description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
},
"webhooks": {
"anyOf": [
{
"$ref": "#/$defs/WebhooksConfig"
},
{
"type": "null"
}
],
"description": "Optional. Webhooks configuration for outbound event delivery.\n\nForwarded into the container as `LANGGRAPH_WEBHOOKS`. See `WebhooksConfig`\nfor URL policy and header templating details.\n"
}
},
"required": [
@@ -616,7 +638,7 @@
},
"EncryptionConfig": {
"title": "EncryptionConfig",
"description": "Configuration for custom at-rest encryption logic.\n\n Allows you to implement custom encryption for sensitive data stored in the database,\n including metadata fields and checkpoint blobs.",
"description": "Configuration for custom at-rest encryption logic.\n\nAllows you to implement custom encryption for sensitive data stored in the database,\nincluding metadata fields and checkpoint blobs.",
"type": "object",
"properties": {
"path": {
@@ -759,13 +781,10 @@
"type": "null"
}
],
"description": "Headers to include (if not also matches against an 'exludes' pattern.\n"
"description": "Headers to include (if not also matched against an 'excludes' pattern).\n"
}
},
"required": [
"excludes",
"includes"
]
"required": []
},
"CorsConfig": {
"title": "CorsConfig",
@@ -908,6 +927,63 @@
}
},
"required": []
},
"WebhooksConfig": {
"title": "WebhooksConfig",
"type": "object",
"properties": {
"env_prefix": {
"type": "string",
"description": "Required prefix for environment variables referenced in header templates.\n\nActs as an allowlist boundary to prevent leaking arbitrary environment\nvariables. Defaults to \"LG_WEBHOOK_\" when omitted.\n"
},
"headers": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "Static headers to include with webhook requests.\n\nValues may contain templates of the form \"${{ env.VAR }}\". On startup, these\nare resolved via the process environment after verifying `VAR` starts with\n`env_prefix`. Mixed literals and multiple templates are allowed.\n"
},
"url": {
"$ref": "#/$defs/WebhookUrlPolicy",
"description": "URL validation policy for user-supplied webhook endpoints."
}
},
"required": [],
"description": "dict() -> new empty dictionary\ndict(mapping) -> new dictionary initialized from a mapping object's\n (key, value) pairs\ndict(iterable) -> new dictionary initialized as if via:\n d = {}\n for k, v in iterable:\n d[k] = v\ndict(**kwargs) -> new dictionary initialized with the name=value pairs\n in the keyword argument list. For example: dict(one=1, two=2)"
},
"WebhookUrlPolicy": {
"title": "WebhookUrlPolicy",
"type": "object",
"properties": {
"allowed_domains": {
"type": "array",
"items": {
"type": "string"
},
"description": "Hostname allowlist. Supports exact hosts and wildcard subdomains.\n\nUse entries like \"hooks.example.com\" or \"*.mycorp.com\". The wildcard only\nmatches subdomains (\"foo.mycorp.com\"), not the apex (\"mycorp.com\"). When\nempty or omitted, any public host is allowed (subject to SSRF IP checks).\n"
},
"allowed_ports": {
"type": "array",
"items": {
"type": "integer"
},
"description": "Explicit port allowlist for absolute URLs.\n\nIf set, requests must use one of these ports. Defaults are respected when\na port is not present in the URL (443 for https, 80 for http).\n"
},
"disable_loopback": {
"type": "boolean",
"description": "Disallow relative URLs (internal loopback calls) when true."
},
"max_url_length": {
"type": "integer",
"description": "Maximum permitted URL length in characters; longer inputs are rejected early."
},
"require_https": {
"type": "boolean",
"description": "Enforce HTTPS scheme for absolute URLs; reject `http://` when true."
}
},
"required": [],
"description": "dict() -> new empty dictionary\ndict(mapping) -> new dictionary initialized from a mapping object's\n (key, value) pairs\ndict(iterable) -> new dictionary initialized as if via:\n d = {}\n for k, v in iterable:\n d[k] = v\ndict(**kwargs) -> new dictionary initialized with the name=value pairs\n in the keyword argument list. For example: dict(one=1, two=2)"
}
},
"title": "LangGraph CLI Configuration",
+82 -6
View File
@@ -210,6 +210,17 @@
}
],
"description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
},
"webhooks": {
"anyOf": [
{
"$ref": "#/$defs/WebhooksConfig"
},
{
"type": "null"
}
],
"description": "Optional. Webhooks configuration for outbound event delivery.\n\nForwarded into the container as `LANGGRAPH_WEBHOOKS`. See `WebhooksConfig`\nfor URL policy and header templating details.\n"
}
},
"required": [
@@ -413,6 +424,17 @@
}
],
"description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
},
"webhooks": {
"anyOf": [
{
"$ref": "#/$defs/WebhooksConfig"
},
{
"type": "null"
}
],
"description": "Optional. Webhooks configuration for outbound event delivery.\n\nForwarded into the container as `LANGGRAPH_WEBHOOKS`. See `WebhooksConfig`\nfor URL policy and header templating details.\n"
}
},
"required": [
@@ -616,7 +638,7 @@
},
"EncryptionConfig": {
"title": "EncryptionConfig",
"description": "Configuration for custom at-rest encryption logic.\n\n Allows you to implement custom encryption for sensitive data stored in the database,\n including metadata fields and checkpoint blobs.",
"description": "Configuration for custom at-rest encryption logic.\n\nAllows you to implement custom encryption for sensitive data stored in the database,\nincluding metadata fields and checkpoint blobs.",
"type": "object",
"properties": {
"path": {
@@ -759,13 +781,10 @@
"type": "null"
}
],
"description": "Headers to include (if not also matches against an 'exludes' pattern.\n"
"description": "Headers to include (if not also matched against an 'excludes' pattern).\n"
}
},
"required": [
"excludes",
"includes"
]
"required": []
},
"CorsConfig": {
"title": "CorsConfig",
@@ -908,6 +927,63 @@
}
},
"required": []
},
"WebhooksConfig": {
"title": "WebhooksConfig",
"type": "object",
"properties": {
"env_prefix": {
"type": "string",
"description": "Required prefix for environment variables referenced in header templates.\n\nActs as an allowlist boundary to prevent leaking arbitrary environment\nvariables. Defaults to \"LG_WEBHOOK_\" when omitted.\n"
},
"headers": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "Static headers to include with webhook requests.\n\nValues may contain templates of the form \"${{ env.VAR }}\". On startup, these\nare resolved via the process environment after verifying `VAR` starts with\n`env_prefix`. Mixed literals and multiple templates are allowed.\n"
},
"url": {
"$ref": "#/$defs/WebhookUrlPolicy",
"description": "URL validation policy for user-supplied webhook endpoints."
}
},
"required": [],
"description": "dict() -> new empty dictionary\ndict(mapping) -> new dictionary initialized from a mapping object's\n (key, value) pairs\ndict(iterable) -> new dictionary initialized as if via:\n d = {}\n for k, v in iterable:\n d[k] = v\ndict(**kwargs) -> new dictionary initialized with the name=value pairs\n in the keyword argument list. For example: dict(one=1, two=2)"
},
"WebhookUrlPolicy": {
"title": "WebhookUrlPolicy",
"type": "object",
"properties": {
"allowed_domains": {
"type": "array",
"items": {
"type": "string"
},
"description": "Hostname allowlist. Supports exact hosts and wildcard subdomains.\n\nUse entries like \"hooks.example.com\" or \"*.mycorp.com\". The wildcard only\nmatches subdomains (\"foo.mycorp.com\"), not the apex (\"mycorp.com\"). When\nempty or omitted, any public host is allowed (subject to SSRF IP checks).\n"
},
"allowed_ports": {
"type": "array",
"items": {
"type": "integer"
},
"description": "Explicit port allowlist for absolute URLs.\n\nIf set, requests must use one of these ports. Defaults are respected when\na port is not present in the URL (443 for https, 80 for http).\n"
},
"disable_loopback": {
"type": "boolean",
"description": "Disallow relative URLs (internal loopback calls) when true."
},
"max_url_length": {
"type": "integer",
"description": "Maximum permitted URL length in characters; longer inputs are rejected early."
},
"require_https": {
"type": "boolean",
"description": "Enforce HTTPS scheme for absolute URLs; reject `http://` when true."
}
},
"required": [],
"description": "dict() -> new empty dictionary\ndict(mapping) -> new dictionary initialized from a mapping object's\n (key, value) pairs\ndict(iterable) -> new dictionary initialized as if via:\n d = {}\n for k, v in iterable:\n d[k] = v\ndict(**kwargs) -> new dictionary initialized with the name=value pairs\n in the keyword argument list. For example: dict(one=1, two=2)"
}
},
"title": "LangGraph CLI Configuration",
+85 -1
View File
@@ -49,6 +49,7 @@ def test_validate_config():
"store": None,
"auth": None,
"encryption": None,
"webhooks": None,
"checkpointer": None,
"http": None,
"ui": None,
@@ -76,6 +77,7 @@ def test_validate_config():
"store": None,
"auth": None,
"encryption": None,
"webhooks": None,
"checkpointer": None,
"http": None,
"ui": None,
@@ -798,7 +800,10 @@ def test_config_to_docker_python_encryption_formatted():
)
# Verify that LANGGRAPH_ENCRYPTION is in the docker output with the correct path
assert "LANGGRAPH_ENCRYPTION=" in actual_docker_stdin
assert "/deps/outer-unit_tests/unit_tests/agent.py:my_encryption" in actual_docker_stdin
assert (
"/deps/outer-unit_tests/unit_tests/agent.py:my_encryption"
in actual_docker_stdin
)
def test_config_to_docker_nodejs_internal_docker_tag():
@@ -834,6 +839,85 @@ RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not foun
assert additional_contexts == {}
def _extract_env_json(dockerfile: str, var_name: str) -> dict:
"""Helper to extract and parse a JSON value from an ENV line in a Dockerfile."""
line_prefix = f"ENV {var_name}='"
for line in dockerfile.splitlines():
if line.startswith(line_prefix) and line.endswith("'"):
json_str = line[len(line_prefix) : -1]
return json.loads(json_str)
raise AssertionError(f"{var_name} not found in Dockerfile env lines")
def test_config_to_docker_webhooks_python():
graphs = {"agent": "./agent.py:graph"}
webhooks = {
"env_prefix": "LG_WEBHOOK_",
"url": {
"require_https": True,
"allowed_domains": ["hooks.example.com", "*.example.org"],
"allowed_ports": [443],
"max_url_length": 1024,
"disable_loopback": False,
},
"headers": {
"x-auth": "${{ env.LG_WEBHOOK_TOKEN }}",
"x-mixed": "Bearer ${{ env.LG_WEBHOOK_TOKEN }}-suffix",
},
}
dockerfile, _ = config_to_docker(
PATH_TO_CONFIG,
validate_config(
{
"dependencies": ["."],
"graphs": graphs,
"webhooks": webhooks,
}
),
"langchain/langgraph-api",
)
# Ensure the ENV line is present and the payload round-trips via JSON
parsed = _extract_env_json(dockerfile, "LANGGRAPH_WEBHOOKS")
assert parsed == webhooks
def test_config_to_docker_webhooks_node():
graphs = {"agent": "./graphs/agent.js:graph"}
webhooks = {
"env_prefix": "LG_WEBHOOK_",
"url": {"require_https": True},
"headers": {"x-auth": "${{ env.LG_WEBHOOK_TOKEN }}"},
}
dockerfile, _ = config_to_docker(
PATH_TO_CONFIG,
validate_config(
{
"node_version": "20",
"graphs": graphs,
"webhooks": webhooks,
}
),
"langchain/langgraphjs-api",
)
parsed = _extract_env_json(dockerfile, "LANGGRAPH_WEBHOOKS")
assert parsed == webhooks
def test_config_to_docker_no_webhooks():
graphs = {"agent": "./agent.py:graph"}
dockerfile, _ = config_to_docker(
PATH_TO_CONFIG,
validate_config({"dependencies": ["."], "graphs": graphs}),
"langchain/langgraph-api",
)
assert "ENV LANGGRAPH_WEBHOOKS=" not in dockerfile
def test_config_to_docker_gen_ui_python():
graphs = {"agent": "./agent.py:graph"}
actual_docker_stdin, additional_contexts = config_to_docker(
+523 -403
View File
File diff suppressed because it is too large Load Diff