mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-30 11:49:38 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52ba71ad60 | ||
|
|
cd05fec1bc |
@@ -509,7 +509,12 @@ def _resolve_env_path(
|
||||
if isinstance(env_field, dict) and env_field:
|
||||
return None
|
||||
if isinstance(env_field, str):
|
||||
env_path = (config_path.parent / env_field).resolve()
|
||||
project_root = config_path.parent.resolve()
|
||||
env_path = (project_root / env_field).resolve()
|
||||
if not env_path.is_relative_to(project_root):
|
||||
raise click.UsageError(
|
||||
f"env file '{env_field}' specified in langgraph.json resolves outside the project directory."
|
||||
)
|
||||
if not env_path.exists():
|
||||
_get_emitter().note(
|
||||
f"Warning: env file '{env_field}' specified in langgraph.json not found."
|
||||
|
||||
@@ -227,6 +227,14 @@ class TestResolveEnvPath:
|
||||
resolved = _resolve_env_path({"env": "custom.env"}, config_path)
|
||||
assert resolved == env_file.resolve()
|
||||
|
||||
@pytest.mark.parametrize("env_field", ["../outside.env", "/etc/passwd"])
|
||||
def test_env_path_outside_project_raises(self, tmp_path, env_field):
|
||||
config_path = tmp_path / "langgraph.json"
|
||||
config_path.touch()
|
||||
|
||||
with pytest.raises(click.UsageError, match="resolves outside the project"):
|
||||
_resolve_env_path({"env": env_field}, config_path)
|
||||
|
||||
def test_missing_env_file_returns_none(self, tmp_path):
|
||||
config_path = tmp_path / "langgraph.json"
|
||||
config_path.touch()
|
||||
|
||||
@@ -392,7 +392,7 @@ class _ResourceOn(typing.Generic[VCreate, VRead, VUpdate, VDelete, VSearch]):
|
||||
def __call__(
|
||||
self,
|
||||
*,
|
||||
resources: str | Sequence[str] | None = None,
|
||||
resources: str | Sequence[str],
|
||||
actions: str | Sequence[str] | None = None,
|
||||
) -> Callable[
|
||||
[_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]],
|
||||
@@ -416,38 +416,25 @@ class _ResourceOn(typing.Generic[VCreate, VRead, VUpdate, VDelete, VSearch]):
|
||||
_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch],
|
||||
]
|
||||
):
|
||||
def register(
|
||||
if fn is not None:
|
||||
_validate_handler(fn)
|
||||
return typing.cast(
|
||||
"_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]",
|
||||
_register_handler(self.auth, self.resource, "*", fn),
|
||||
)
|
||||
|
||||
def decorator(
|
||||
handler: _ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch],
|
||||
) -> _ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]:
|
||||
_validate_handler(handler)
|
||||
if isinstance(resources, str):
|
||||
resource_list = [resources]
|
||||
else:
|
||||
resource_list = (
|
||||
list(resources) if resources is not None else [self.resource]
|
||||
)
|
||||
if resource_list != [self.resource]:
|
||||
raise ValueError(
|
||||
f"Resource-specific decorator for {self.resource!r} cannot "
|
||||
f"register handlers for {resource_list!r}. Use @auth.on(...) "
|
||||
"for multiple resources."
|
||||
)
|
||||
if isinstance(actions, str):
|
||||
action_list = [actions]
|
||||
else:
|
||||
action_list = list(actions) if actions is not None else ["*"]
|
||||
for action in action_list:
|
||||
_register_handler(self.auth, self.resource, action, handler)
|
||||
return handler
|
||||
|
||||
if fn is not None:
|
||||
return register(
|
||||
typing.cast(
|
||||
"_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]",
|
||||
fn,
|
||||
)
|
||||
return typing.cast(
|
||||
"_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]",
|
||||
_register_handler(self.auth, self.resource, "*", handler),
|
||||
)
|
||||
return register
|
||||
|
||||
# Accept keyword-only parameters for future filtering behavior; referenced to satisfy linters.
|
||||
_ = resources, actions
|
||||
return decorator
|
||||
|
||||
|
||||
class _AssistantsOn(
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from langgraph_sdk import Auth
|
||||
|
||||
|
||||
def _handler():
|
||||
async def handler(ctx, value):
|
||||
return ctx is not None and value is not None
|
||||
|
||||
return handler
|
||||
|
||||
|
||||
def test_resource_decorator_registers_specific_actions():
|
||||
auth = Auth()
|
||||
|
||||
handler = auth.on.threads(actions=["read", "search"])(_handler())
|
||||
|
||||
assert auth._handlers == {
|
||||
("threads", "read"): [handler],
|
||||
("threads", "search"): [handler],
|
||||
}
|
||||
|
||||
|
||||
def test_resource_decorator_registers_single_action():
|
||||
auth = Auth()
|
||||
|
||||
handler = auth.on.threads(actions="read")(_handler())
|
||||
|
||||
assert auth._handlers == {("threads", "read"): [handler]}
|
||||
|
||||
|
||||
def test_resource_decorator_without_actions_registers_resource_wildcard():
|
||||
auth = Auth()
|
||||
|
||||
handler = auth.on.threads(_handler())
|
||||
|
||||
assert auth._handlers == {("threads", "*"): [handler]}
|
||||
|
||||
|
||||
def test_resource_decorator_rejects_mismatched_resources():
|
||||
auth = Auth()
|
||||
|
||||
with pytest.raises(ValueError, match=r"Use @auth\.on"):
|
||||
auth.on.threads(resources="assistants", actions="read")(_handler())
|
||||
Reference in New Issue
Block a user