langgraph-api 0.4.1__py3-none-any.whl → 0.7.3__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_api/__init__.py +1 -1
- langgraph_api/api/__init__.py +111 -51
- langgraph_api/api/a2a.py +1610 -0
- langgraph_api/api/assistants.py +212 -89
- langgraph_api/api/mcp.py +3 -3
- langgraph_api/api/meta.py +52 -28
- langgraph_api/api/openapi.py +27 -17
- langgraph_api/api/profile.py +108 -0
- langgraph_api/api/runs.py +342 -195
- langgraph_api/api/store.py +19 -2
- langgraph_api/api/threads.py +209 -27
- langgraph_api/asgi_transport.py +14 -9
- langgraph_api/asyncio.py +14 -4
- langgraph_api/auth/custom.py +52 -37
- langgraph_api/auth/langsmith/backend.py +4 -3
- langgraph_api/auth/langsmith/client.py +13 -8
- langgraph_api/cli.py +230 -133
- langgraph_api/command.py +5 -3
- langgraph_api/config/__init__.py +532 -0
- langgraph_api/config/_parse.py +58 -0
- langgraph_api/config/schemas.py +431 -0
- langgraph_api/cron_scheduler.py +17 -1
- langgraph_api/encryption/__init__.py +15 -0
- langgraph_api/encryption/aes_json.py +158 -0
- langgraph_api/encryption/context.py +35 -0
- langgraph_api/encryption/custom.py +280 -0
- langgraph_api/encryption/middleware.py +632 -0
- langgraph_api/encryption/shared.py +63 -0
- langgraph_api/errors.py +12 -1
- langgraph_api/executor_entrypoint.py +11 -6
- langgraph_api/feature_flags.py +29 -0
- langgraph_api/graph.py +176 -76
- langgraph_api/grpc/client.py +313 -0
- langgraph_api/grpc/config_conversion.py +231 -0
- langgraph_api/grpc/generated/__init__.py +29 -0
- langgraph_api/grpc/generated/checkpointer_pb2.py +63 -0
- langgraph_api/grpc/generated/checkpointer_pb2.pyi +99 -0
- langgraph_api/grpc/generated/checkpointer_pb2_grpc.py +329 -0
- langgraph_api/grpc/generated/core_api_pb2.py +216 -0
- langgraph_api/grpc/generated/core_api_pb2.pyi +905 -0
- langgraph_api/grpc/generated/core_api_pb2_grpc.py +1621 -0
- langgraph_api/grpc/generated/engine_common_pb2.py +219 -0
- langgraph_api/grpc/generated/engine_common_pb2.pyi +722 -0
- langgraph_api/grpc/generated/engine_common_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_cancel_run_action_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_cancel_run_action_pb2.pyi +12 -0
- langgraph_api/grpc/generated/enum_cancel_run_action_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_control_signal_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_control_signal_pb2.pyi +16 -0
- langgraph_api/grpc/generated/enum_control_signal_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_durability_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_durability_pb2.pyi +16 -0
- langgraph_api/grpc/generated/enum_durability_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_multitask_strategy_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_multitask_strategy_pb2.pyi +16 -0
- langgraph_api/grpc/generated/enum_multitask_strategy_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_run_status_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_run_status_pb2.pyi +22 -0
- langgraph_api/grpc/generated/enum_run_status_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_stream_mode_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_stream_mode_pb2.pyi +28 -0
- langgraph_api/grpc/generated/enum_stream_mode_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_thread_status_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_thread_status_pb2.pyi +16 -0
- langgraph_api/grpc/generated/enum_thread_status_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_thread_stream_mode_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_thread_stream_mode_pb2.pyi +16 -0
- langgraph_api/grpc/generated/enum_thread_stream_mode_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/errors_pb2.py +39 -0
- langgraph_api/grpc/generated/errors_pb2.pyi +21 -0
- langgraph_api/grpc/generated/errors_pb2_grpc.py +24 -0
- langgraph_api/grpc/ops/__init__.py +370 -0
- langgraph_api/grpc/ops/assistants.py +424 -0
- langgraph_api/grpc/ops/runs.py +792 -0
- langgraph_api/grpc/ops/threads.py +1013 -0
- langgraph_api/http.py +16 -5
- langgraph_api/http_metrics.py +15 -35
- langgraph_api/http_metrics_utils.py +38 -0
- langgraph_api/js/build.mts +1 -1
- langgraph_api/js/client.http.mts +13 -7
- langgraph_api/js/client.mts +2 -5
- langgraph_api/js/package.json +29 -28
- langgraph_api/js/remote.py +56 -30
- langgraph_api/js/src/graph.mts +20 -0
- langgraph_api/js/sse.py +2 -2
- langgraph_api/js/ui.py +1 -1
- langgraph_api/js/yarn.lock +1204 -1006
- langgraph_api/logging.py +29 -2
- langgraph_api/metadata.py +99 -28
- langgraph_api/middleware/http_logger.py +7 -2
- langgraph_api/middleware/private_network.py +7 -7
- langgraph_api/models/run.py +54 -93
- langgraph_api/otel_context.py +205 -0
- langgraph_api/patch.py +5 -3
- langgraph_api/queue_entrypoint.py +154 -65
- langgraph_api/route.py +47 -5
- langgraph_api/schema.py +88 -10
- langgraph_api/self_hosted_logs.py +124 -0
- langgraph_api/self_hosted_metrics.py +450 -0
- langgraph_api/serde.py +79 -37
- langgraph_api/server.py +138 -60
- langgraph_api/state.py +4 -3
- langgraph_api/store.py +25 -16
- langgraph_api/stream.py +80 -29
- langgraph_api/thread_ttl.py +31 -13
- langgraph_api/timing/__init__.py +25 -0
- langgraph_api/timing/profiler.py +200 -0
- langgraph_api/timing/timer.py +318 -0
- langgraph_api/utils/__init__.py +53 -8
- langgraph_api/utils/cache.py +47 -10
- langgraph_api/utils/config.py +2 -1
- langgraph_api/utils/errors.py +77 -0
- langgraph_api/utils/future.py +10 -6
- langgraph_api/utils/headers.py +76 -2
- langgraph_api/utils/retriable_client.py +74 -0
- langgraph_api/utils/stream_codec.py +315 -0
- langgraph_api/utils/uuids.py +29 -62
- langgraph_api/validation.py +9 -0
- langgraph_api/webhook.py +120 -6
- langgraph_api/worker.py +55 -24
- {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/METADATA +16 -8
- langgraph_api-0.7.3.dist-info/RECORD +168 -0
- {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/WHEEL +1 -1
- langgraph_runtime/__init__.py +1 -0
- langgraph_runtime/routes.py +11 -0
- logging.json +1 -3
- openapi.json +839 -478
- langgraph_api/config.py +0 -387
- langgraph_api/js/isolate-0x130008000-46649-46649-v8.log +0 -4430
- langgraph_api/js/isolate-0x138008000-44681-44681-v8.log +0 -4430
- langgraph_api/js/package-lock.json +0 -3308
- langgraph_api-0.4.1.dist-info/RECORD +0 -107
- /langgraph_api/{utils.py → grpc/__init__.py} +0 -0
- {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/entry_points.txt +0 -0
- {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""Custom encryption loading for LangGraph API.
|
|
2
|
+
|
|
3
|
+
This module provides functions to load and access custom encryption
|
|
4
|
+
instances defined by users in their langgraph.json configuration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import functools
|
|
10
|
+
import importlib.util
|
|
11
|
+
import sys
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Literal, get_args
|
|
13
|
+
|
|
14
|
+
import structlog
|
|
15
|
+
|
|
16
|
+
from langgraph_api import timing
|
|
17
|
+
from langgraph_api.config import LANGGRAPH_ENCRYPTION
|
|
18
|
+
from langgraph_api.encryption.shared import (
|
|
19
|
+
ENCRYPTION_CONTEXT_KEY,
|
|
20
|
+
strip_encryption_metadata,
|
|
21
|
+
)
|
|
22
|
+
from langgraph_api.timing import profiled_import
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from langgraph_sdk import Encryption
|
|
26
|
+
from langgraph_sdk.encryption.types import JsonDecryptor, JsonEncryptor
|
|
27
|
+
|
|
28
|
+
from langgraph_api.encryption.aes_json import AesEncryptionInstance
|
|
29
|
+
|
|
30
|
+
ModelType = Literal["run", "thread", "assistant", "cron", "checkpoint"]
|
|
31
|
+
SUPPORTED_ENCRYPTION_MODELS: frozenset[str] = frozenset(get_args(ModelType))
|
|
32
|
+
|
|
33
|
+
logger = structlog.stdlib.get_logger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@functools.lru_cache(maxsize=1)
|
|
37
|
+
def get_custom_encryption_instance() -> Encryption | None:
|
|
38
|
+
"""Get the custom (SDK-injected) encryption instance if configured.
|
|
39
|
+
|
|
40
|
+
Custom encryption is user-defined encryption logic loaded from a Python module
|
|
41
|
+
specified in langgraph.json via LANGGRAPH_ENCRYPTION config.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
The Encryption instance if configured, or None if no custom encryption is configured.
|
|
45
|
+
"""
|
|
46
|
+
if not LANGGRAPH_ENCRYPTION:
|
|
47
|
+
return None
|
|
48
|
+
logger.info(
|
|
49
|
+
f"Getting custom encryption instance: {LANGGRAPH_ENCRYPTION}",
|
|
50
|
+
langgraph_encryption=str(LANGGRAPH_ENCRYPTION),
|
|
51
|
+
)
|
|
52
|
+
path = LANGGRAPH_ENCRYPTION.get("path")
|
|
53
|
+
if path is None:
|
|
54
|
+
return None
|
|
55
|
+
return _load_custom_encryption(path)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _load_custom_encryption(path: str) -> Encryption:
|
|
59
|
+
"""Load a custom encryption instance from a module path.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
path: Module path in format "./path/to/file.py:name" or "module:name"
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
The custom Encryption instance.
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
ValueError: If the path is invalid or the encryption instance is not found.
|
|
69
|
+
"""
|
|
70
|
+
encryption_instance = _load_encryption_obj(path)
|
|
71
|
+
logger.info(
|
|
72
|
+
f"Loaded custom encryption instance from path {path}: {encryption_instance}"
|
|
73
|
+
)
|
|
74
|
+
return encryption_instance
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@timing.timer(
|
|
78
|
+
message="Loading custom encryption {encryption_path}",
|
|
79
|
+
metadata_fn=lambda encryption_path: {"encryption_path": encryption_path},
|
|
80
|
+
warn_threshold_secs=5,
|
|
81
|
+
warn_message="Loading custom encryption '{encryption_path}' took longer than expected",
|
|
82
|
+
error_threshold_secs=10,
|
|
83
|
+
)
|
|
84
|
+
def _load_encryption_obj(path: str) -> Encryption:
|
|
85
|
+
"""Load an Encryption object from a path string.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
path: Module path in format "./path/to/file.py:name" or "module:name"
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
The Encryption instance.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
ValueError: If the path is invalid or the encryption instance is not found.
|
|
95
|
+
ImportError: If the module cannot be imported.
|
|
96
|
+
FileNotFoundError: If the file cannot be found.
|
|
97
|
+
"""
|
|
98
|
+
if ":" not in path:
|
|
99
|
+
raise ValueError(
|
|
100
|
+
f"Invalid encryption path format: {path}. "
|
|
101
|
+
"Must be in format: './path/to/file.py:name' or 'module:name'"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
module_name, callable_name = path.rsplit(":", 1)
|
|
105
|
+
module_name = module_name.rstrip(":")
|
|
106
|
+
|
|
107
|
+
if module_name.endswith(".js") or module_name.endswith(".mjs"):
|
|
108
|
+
raise ValueError(
|
|
109
|
+
f"JavaScript encryption is not supported. "
|
|
110
|
+
f"Please use a Python module instead: {module_name}"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
with profiled_import(path):
|
|
115
|
+
if "/" in module_name or ".py" in module_name:
|
|
116
|
+
modname = f"dynamic_module_{hash(module_name)}"
|
|
117
|
+
modspec = importlib.util.spec_from_file_location(modname, module_name)
|
|
118
|
+
if modspec is None or modspec.loader is None:
|
|
119
|
+
raise ValueError(f"Could not load file: {module_name}")
|
|
120
|
+
module = importlib.util.module_from_spec(modspec)
|
|
121
|
+
sys.modules[modname] = module
|
|
122
|
+
modspec.loader.exec_module(module)
|
|
123
|
+
else:
|
|
124
|
+
module = importlib.import_module(module_name)
|
|
125
|
+
|
|
126
|
+
loaded_encrypt = getattr(module, callable_name, None)
|
|
127
|
+
if loaded_encrypt is None:
|
|
128
|
+
raise ValueError(
|
|
129
|
+
f"Could not find encrypt '{callable_name}' in module: {module_name}"
|
|
130
|
+
)
|
|
131
|
+
# Import Encryption at runtime only when needed (avoids requiring SDK 0.2.14)
|
|
132
|
+
from langgraph_sdk import Encryption as EncryptionClass
|
|
133
|
+
|
|
134
|
+
if not isinstance(loaded_encrypt, EncryptionClass):
|
|
135
|
+
raise ValueError(
|
|
136
|
+
f"Expected an Encryption instance, got {type(loaded_encrypt)}"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return loaded_encrypt
|
|
140
|
+
|
|
141
|
+
except ImportError as e:
|
|
142
|
+
e.add_note(f"Could not import module:\n{module_name}\n\n")
|
|
143
|
+
raise
|
|
144
|
+
except FileNotFoundError as e:
|
|
145
|
+
raise ValueError(f"Could not find file: {module_name}") from e
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class JsonEncryptionWrapper:
|
|
149
|
+
"""Wrapper for JSON encryption that routes between custom and AES encryption.
|
|
150
|
+
|
|
151
|
+
This wrapper handles dual-mode encryption routing:
|
|
152
|
+
- Encrypts using custom (SDK-injected) encryption when configured
|
|
153
|
+
- Decrypts using either custom OR AES based on the encryption context marker
|
|
154
|
+
- Supports migration from AES-only to custom encryption (reads old AES data)
|
|
155
|
+
|
|
156
|
+
Key responsibilities:
|
|
157
|
+
- Key preservation validation (encryptor must not add/remove keys)
|
|
158
|
+
- Encryption context storage (adds __encryption_context__ marker)
|
|
159
|
+
- Migration routing (AES-encrypted data routes to AES decryptor)
|
|
160
|
+
- Defensive checks (AES values in custom path raises error)
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
def __init__(
|
|
164
|
+
self,
|
|
165
|
+
custom_instance: Encryption,
|
|
166
|
+
aes_instance: AesEncryptionInstance | None = None,
|
|
167
|
+
) -> None:
|
|
168
|
+
"""Initialize with custom encryption and optional AES for migration.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
custom_instance: The SDK's Encryption instance (custom/user-defined encryption)
|
|
172
|
+
aes_instance: Optional AES instance for migration (decrypting old AES data)
|
|
173
|
+
"""
|
|
174
|
+
self._custom = custom_instance
|
|
175
|
+
self._aes = aes_instance
|
|
176
|
+
|
|
177
|
+
def get_json_encryptor(self, model_type: ModelType) -> JsonEncryptor | None:
|
|
178
|
+
"""Return an async encryptor that validates keys and adds context.
|
|
179
|
+
|
|
180
|
+
The encryptor:
|
|
181
|
+
1. Calls the custom encryptor
|
|
182
|
+
2. Validates key preservation (no added/removed keys)
|
|
183
|
+
3. Adds __encryption_context__ with the user's context
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
model_type: The type of model being encrypted
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Async encryptor function, or None if custom has no encryptor
|
|
190
|
+
"""
|
|
191
|
+
from langgraph_api.encryption.aes_json import EncryptionKeyError
|
|
192
|
+
|
|
193
|
+
custom_encryptor = self._custom.get_json_encryptor(model_type)
|
|
194
|
+
if custom_encryptor is None:
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
async def encryptor(ctx: Any, data: dict[str, Any]) -> dict[str, Any]:
|
|
198
|
+
encrypted = await custom_encryptor(ctx, data)
|
|
199
|
+
|
|
200
|
+
# Validate key preservation for SQL JSONB merge compatibility
|
|
201
|
+
if encrypted is not None and isinstance(encrypted, dict):
|
|
202
|
+
input_keys = set(data.keys())
|
|
203
|
+
output_keys = set(encrypted.keys())
|
|
204
|
+
added_keys = output_keys - input_keys
|
|
205
|
+
removed_keys = input_keys - output_keys
|
|
206
|
+
if added_keys or removed_keys:
|
|
207
|
+
raise EncryptionKeyError(
|
|
208
|
+
f"JSON encryptor must preserve key structure for SQL JSONB merge compatibility. "
|
|
209
|
+
f"Added keys: {added_keys or 'none'}, removed keys: {removed_keys or 'none'}. "
|
|
210
|
+
f"Use per-key encryption (transform values, not keys) instead of envelope patterns."
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Add encryption context marker with user's context
|
|
214
|
+
from langgraph_api.encryption.context import get_encryption_context
|
|
215
|
+
|
|
216
|
+
encrypted[ENCRYPTION_CONTEXT_KEY] = get_encryption_context()
|
|
217
|
+
|
|
218
|
+
return encrypted
|
|
219
|
+
|
|
220
|
+
return encryptor
|
|
221
|
+
|
|
222
|
+
def get_json_decryptor(self, model_type: ModelType) -> JsonDecryptor:
|
|
223
|
+
"""Return an async decryptor that routes based on encryption type.
|
|
224
|
+
|
|
225
|
+
The decryptor routes based on __encryption_context__ marker:
|
|
226
|
+
1. AES type marker → AES decryptor (migration path)
|
|
227
|
+
2. Custom marker → custom decryptor (with defensive check)
|
|
228
|
+
3. No marker → passthrough (plaintext)
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
model_type: The type of model being decrypted
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Async decryptor function (always returns a function for routing)
|
|
235
|
+
"""
|
|
236
|
+
from langgraph_api.encryption.aes_json import (
|
|
237
|
+
DecryptorMissingError,
|
|
238
|
+
EncryptionRoutingError,
|
|
239
|
+
has_any_aes_encrypted_values,
|
|
240
|
+
is_aes_encryption_context,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
custom_decryptor = self._custom.get_json_decryptor(model_type)
|
|
244
|
+
|
|
245
|
+
async def decryptor(ctx: Any, data: dict[str, Any]) -> dict[str, Any]:
|
|
246
|
+
# No marker → plaintext passthrough
|
|
247
|
+
if ENCRYPTION_CONTEXT_KEY not in data:
|
|
248
|
+
return strip_encryption_metadata(data)
|
|
249
|
+
|
|
250
|
+
context_dict = data[ENCRYPTION_CONTEXT_KEY]
|
|
251
|
+
|
|
252
|
+
# AES type marker → route to AES decryptor (migration path)
|
|
253
|
+
if is_aes_encryption_context(context_dict):
|
|
254
|
+
if self._aes is None:
|
|
255
|
+
raise DecryptorMissingError(
|
|
256
|
+
f"Data has AES encryption marker but LANGGRAPH_AES_KEY is not configured "
|
|
257
|
+
f"for {model_type}"
|
|
258
|
+
)
|
|
259
|
+
aes_decryptor = self._aes.get_json_decryptor(model_type)
|
|
260
|
+
return await aes_decryptor(ctx, data)
|
|
261
|
+
|
|
262
|
+
# Custom marker → use custom decryptor
|
|
263
|
+
if custom_decryptor is None:
|
|
264
|
+
raise DecryptorMissingError(
|
|
265
|
+
f"Data contains custom encryption marker but no decryptor is configured for {model_type}"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Defensive check: ensure custom decryptor doesn't receive AES-encrypted values
|
|
269
|
+
if has_any_aes_encrypted_values(data):
|
|
270
|
+
raise EncryptionRoutingError(
|
|
271
|
+
f"Data has AES-encrypted values but is being routed to custom decryptor. "
|
|
272
|
+
f"This indicates a bug in encryption routing for {model_type}."
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# Strip marker and decrypt
|
|
276
|
+
data = strip_encryption_metadata(data)
|
|
277
|
+
decrypted = await custom_decryptor(ctx, data)
|
|
278
|
+
return strip_encryption_metadata(decrypted)
|
|
279
|
+
|
|
280
|
+
return decryptor
|