From 5ac837d7cd64b2a97a1575f1e35af8e7f73d73ed Mon Sep 17 00:00:00 2001 From: William FH <13333726+hinthornw@users.noreply.github.com> Date: Thu, 19 Feb 2026 07:16:26 -0800 Subject: [PATCH] feat(sdk-py): improve store auth type safety and docstrings (#6867) ## Summary - Add `_StoreActionOn` class with action-specific decorator properties (`.put`, `.get`, `.search`, `.delete`, `.list_namespaces`) to `_StoreOn`, enabling `@auth.on.store.put` etc. which was documented but not implemented - Update docstring examples to show namespace-rewriting pattern as the canonical store auth approach - Fix `AuthContext.action` docstring: add missing `search` and `delete` store actions - Fix typo in `StoreSearch.query` docstring - Add auth-handler-aware docstrings to all store TypedDicts ## Test plan - [x] `make format && make lint` passes in `libs/sdk-py` - [] Verify `@auth.on.store.put` decorator works at runtime Co-authored-by: Claude Opus 4.6 --- libs/sdk-py/langgraph_sdk/auth/__init__.py | 112 +++++++++++++++++++-- libs/sdk-py/langgraph_sdk/auth/types.py | 66 +++++++++--- 2 files changed, 158 insertions(+), 20 deletions(-) diff --git a/libs/sdk-py/langgraph_sdk/auth/__init__.py b/libs/sdk-py/langgraph_sdk/auth/__init__.py index c12502e5f..3655d0f22 100644 --- a/libs/sdk-py/langgraph_sdk/auth/__init__.py +++ b/libs/sdk-py/langgraph_sdk/auth/__init__.py @@ -72,8 +72,13 @@ class Auth: assert params.get("metadata", {}).get("owner") == "allowed_user" @auth.on.store - async def authorize_store(ctx: Auth.types.AuthContext, value: Auth.types.on): - assert ctx.user.identity in value["namespace"], "Not authorized" + async def authorize_store(ctx: Auth.types.AuthContext, value: Auth.types.on.store.value): + # Automatically scope all store operations to the user's namespace. + namespace = tuple(value["namespace"]) if value.get("namespace") else () + assert isinstance(namespace, tuple) + if not namespace or namespace[0] != ctx.user.identity: + namespace = (ctx.user.identity, *namespace) + value["namespace"] = namespace ``` ???+ note "Request Processing Flow" @@ -170,13 +175,32 @@ class Auth: ``` Auth for the `store` resource is a bit different since its structure is developer defined. - You typically want to enforce user creds in the namespace. + You typically want to scope store operations by rewriting the namespace to include the user's identity. + The `value` dict is mutable — changes to `value["namespace"]` are used by the server for the actual operation. ```python @auth.on.store - async def check_store_access(ctx: AuthContext, value: Auth.types.on) -> bool: - # Assuming you structure your store like (store.aput((user_id, application_context), key, value)) - assert value["namespace"][0] == ctx.user.identity + async def authorize_store(ctx: AuthContext, value: Auth.types.on.store.value): + # Automatically scope all store operations to the user's namespace. + namespace = tuple(value["namespace"]) if value.get("namespace") else () + assert isinstance(namespace, tuple) + if not namespace or namespace[0] != ctx.user.identity: + namespace = (ctx.user.identity, *namespace) + value["namespace"] = namespace + ``` + + You can also register handlers for specific store actions: + + ```python + @auth.on.store.put + async def on_put(ctx: AuthContext, value: Auth.types.on.store.put.value): + # value has typed fields: namespace, key, value, index + ... + + @auth.on.store.get + async def on_get(ctx: AuthContext, value: Auth.types.on.store.get.value): + # value has typed fields: namespace, key + ... ``` """ # These are accessed by the API. Changes to their names or types is @@ -483,9 +507,85 @@ class _CronsOn( Search = types.CronsSearch +class _StoreActionOn(typing.Generic[T]): + """Decorator for registering a handler for a specific store action.""" + + def __init__( + self, + auth: Auth, + action: typing.Literal["put", "get", "search", "delete", "list_namespaces"], + value: type[T], + ) -> None: + self.auth = auth + self.action = action + self.value = value + + def __call__(self, fn: _ActionHandler[T]) -> _ActionHandler[T]: + _validate_handler(fn) + _register_handler(self.auth, "store", self.action, fn) + return fn + + class _StoreOn: def __init__(self, auth: Auth) -> None: self._auth = auth + self.put = _StoreActionOn(auth, "put", types.StorePut) + """Register a handler for store put operations. + + ???+ example "Example" + ```python + @auth.on.store.put + async def on_store_put(ctx: Auth.types.AuthContext, value: Auth.types.on.store.put.value): + # Scope puts to user's namespace + ... + ``` + """ + self.get = _StoreActionOn(auth, "get", types.StoreGet) + """Register a handler for store get operations. + + ???+ example "Example" + ```python + @auth.on.store.get + async def on_store_get(ctx: Auth.types.AuthContext, value: Auth.types.on.store.get.value): + # Scope gets to user's namespace + ... + ``` + """ + self.search = _StoreActionOn(auth, "search", types.StoreSearch) + """Register a handler for store search operations. + + ???+ example "Example" + ```python + @auth.on.store.search + async def on_store_search(ctx: Auth.types.AuthContext, value: Auth.types.on.store.search.value): + # Scope searches to user's namespace + ... + ``` + """ + self.delete = _StoreActionOn(auth, "delete", types.StoreDelete) + """Register a handler for store delete operations. + + ???+ example "Example" + ```python + @auth.on.store.delete + async def on_store_delete(ctx: Auth.types.AuthContext, value: Auth.types.on.store.delete.value): + # Scope deletes to user's namespace + ... + ``` + """ + self.list_namespaces = _StoreActionOn( + auth, "list_namespaces", types.StoreListNamespaces + ) + """Register a handler for store list_namespaces operations. + + ???+ example "Example" + ```python + @auth.on.store.list_namespaces + async def on_list_ns(ctx: Auth.types.AuthContext, value: Auth.types.on.store.list_namespaces.value): + # Scope namespace listing to user's prefix + ... + ``` + """ @typing.overload def __call__( diff --git a/libs/sdk-py/langgraph_sdk/auth/types.py b/libs/sdk-py/langgraph_sdk/auth/types.py index e5d29ea2f..ded601112 100644 --- a/libs/sdk-py/langgraph_sdk/auth/types.py +++ b/libs/sdk-py/langgraph_sdk/auth/types.py @@ -402,7 +402,7 @@ class AuthContext(BaseAuthContext): "list_namespaces", ] """The action being performed on the resource. - + Most resources support the following actions: - create: Create a new resource - read: Read information about a resource @@ -411,8 +411,10 @@ class AuthContext(BaseAuthContext): - search: Search for resources The store supports the following actions: - - put: Add or update a document in the store - - get: Get a document from the store + - put: Add or update an item in the store + - get: Get an item from the store + - search: Search for items within a namespace prefix + - delete: Delete an item from the store - list_namespaces: List the namespaces in the store """ @@ -851,20 +853,34 @@ class CronsSearch(typing.TypedDict, total=False): class StoreGet(typing.TypedDict): - """Operation to retrieve a specific item by its namespace and key.""" + """Operation to retrieve a specific item by its namespace and key. + + This dict is mutable — auth handlers can modify `namespace` to enforce + access scoping (e.g., prepending the user's identity). + """ namespace: tuple[str, ...] - """Hierarchical path that uniquely identifies the item's location.""" + """Hierarchical path that uniquely identifies the item's location. + + Auth handlers can modify this to enforce per-user scoping. + """ key: str """Unique identifier for the item within its specific namespace.""" class StoreSearch(typing.TypedDict): - """Operation to search for items within a specified namespace hierarchy.""" + """Operation to search for items within a specified namespace hierarchy. + + This dict is mutable — auth handlers can modify `namespace` to enforce + access scoping (e.g., prepending the user's identity). + """ namespace: tuple[str, ...] - """Prefix filter for defining the search scope.""" + """Prefix filter for defining the search scope. + + Auth handlers can modify this to enforce per-user scoping. + """ filter: dict[str, typing.Any] | None """Key-value pairs for filtering results based on exact matches or comparison operators.""" @@ -876,14 +892,22 @@ class StoreSearch(typing.TypedDict): """Number of matching items to skip for pagination.""" query: str | None - """Naturalj language search query for semantic search capabilities.""" + """Natural language search query for semantic search capabilities.""" class StoreListNamespaces(typing.TypedDict): - """Operation to list and filter namespaces in the store.""" + """Operation to list and filter namespaces in the store. + + This dict is mutable — auth handlers can modify `namespace` (the prefix) + to enforce access scoping (e.g., prepending the user's identity). + """ namespace: tuple[str, ...] | None - """Prefix filter namespaces.""" + """Prefix filter for namespaces. Can be `None` if no prefix was provided. + + Auth handlers can modify this to enforce per-user scoping. When `None`, + handlers should set it to `(user_id,)` to scope listing to the user's namespaces. + """ suffix: tuple[str, ...] | None """Optional conditions for filtering namespaces.""" @@ -903,10 +927,17 @@ class StoreListNamespaces(typing.TypedDict): class StorePut(typing.TypedDict): - """Operation to store, update, or delete an item in the store.""" + """Operation to store, update, or delete an item in the store. + + This dict is mutable — auth handlers can modify `namespace` to enforce + access scoping (e.g., prepending the user's identity). + """ namespace: tuple[str, ...] - """Hierarchical path that identifies the location of the item.""" + """Hierarchical path that identifies the location of the item. + + Auth handlers can modify this to enforce per-user scoping. + """ key: str """Unique identifier for the item within its namespace.""" @@ -919,10 +950,17 @@ class StorePut(typing.TypedDict): class StoreDelete(typing.TypedDict): - """Operation to delete an item from the store.""" + """Operation to delete an item from the store. + + This dict is mutable — auth handlers can modify `namespace` to enforce + access scoping (e.g., prepending the user's identity). + """ namespace: tuple[str, ...] - """Hierarchical path that uniquely identifies the item's location.""" + """Hierarchical path that uniquely identifies the item's location. + + Auth handlers can modify this to enforce per-user scoping. + """ key: str """Unique identifier for the item within its specific namespace."""