Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7773bed
chore: Ignore docs/superpowers/ from version control
sohanmaheshwar Apr 15, 2026
b03c728
security: Remove fail_open from SpiceDBAuthorizer — errors now always…
sohanmaheshwar Apr 15, 2026
09db7d7
security: Remove fail_open from all components — breaking change, v0.1.0
sohanmaheshwar Apr 15, 2026
987e96a
style: Fix blank lines in test_tools.py after fail_open removal
sohanmaheshwar Apr 15, 2026
cd8c501
feat: Add lookup_resources() to SpiceDBAuthorizer
sohanmaheshwar Apr 15, 2026
74dee3e
test: Strengthen lookup_resources unit test assertions
sohanmaheshwar Apr 15, 2026
8780602
feat: Add SpiceDBPreFilterRetriever with LookupResources pre-filtering
sohanmaheshwar Apr 15, 2026
ab8ab0e
fix: Use is not None guard in with_config, strengthen k test
sohanmaheshwar Apr 15, 2026
30df202
docs: Add pre-filter to README decision table and add example
sohanmaheshwar Apr 15, 2026
033b978
docs: Fix dead import in example, add pre-filter to README features
sohanmaheshwar Apr 15, 2026
b72eef4
breaking: Remove SpiceDBRetriever — use base_retriever | SpiceDBAuthF…
sohanmaheshwar May 19, 2026
f7f41ff
breaking: Remove SpiceDBAuthLambda — use SpiceDBAuthFilter directly i…
sohanmaheshwar May 19, 2026
c1950ec
feat: Add create_pre_filter_auth_node for LangGraph pre-filter author…
sohanmaheshwar May 19, 2026
4cd7dd3
fix: Update tool_example prerequisites with schema including edit/del…
sohanmaheshwar May 19, 2026
71fa414
fix: Make tool_example self-contained — write schema and relationship…
sohanmaheshwar May 19, 2026
e8c7efa
fix: Make langgraph_visualization_example self-contained, remove dead…
sohanmaheshwar May 19, 2026
a23c903
refactor: Rename langgraph_visualization_example to langgraph_postfil…
sohanmaheshwar May 19, 2026
4fced71
fix: Expand langgraph_postfilter_example schema to avoid conflict wit…
sohanmaheshwar May 19, 2026
ae34431
feat: Add real LLM answer generation to langgraph_postfilter_example
sohanmaheshwar May 19, 2026
4b7b008
chore: Remove Method 4 side-by-side comparison and summary from langg…
sohanmaheshwar May 19, 2026
cd8980c
feat: Add SpiceDBPreFilterRetriever and create_pre_filter_auth_node e…
sohanmaheshwar May 19, 2026
3d11dd4
docs: Rewrite READMEs — add missing components to main, overhaul exam…
sohanmaheshwar May 19, 2026
0f7b792
refactor: Rename create_auth_node→create_check_permissions_node, crea…
sohanmaheshwar May 19, 2026
e187ce7
chore: Bump version to 0.2.0, add CHANGELOG with breaking changes
sohanmaheshwar May 19, 2026
7bc6ba0
fix: Replace lambda assignment with def to satisfy ruff E731
sohanmaheshwar May 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions .env.example

This file was deleted.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Thumbs.db

# Docs
docs/_build/
docs/superpowers/
claude_notes.md
claude.md

Expand Down
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.0] - 2026-05-19

### Breaking Changes

| Symbol | Change | Migration |
|--------|--------|-----------|
| `SpiceDBRetriever` | Removed | Use `base_retriever \| SpiceDBAuthFilter` and pass `subject_id` via `config={"configurable": {"subject_id": "alice"}}` |
| `SpiceDBAuthLambda` | Removed | Use `SpiceDBAuthFilter` directly in chains: `RunnableLambda(retriever) \| auth_filter` |
| `create_auth_node` | Renamed to `create_check_permissions_node` | Update imports and call sites |
| `fail_open` parameter | Removed from all components | Remove the argument — errors now propagate instead of silently granting access |

### Added

- `SpiceDBPreFilterRetriever` — LangChain `BaseRetriever` using SpiceDB's `LookupResources` API. Fetches authorized resource IDs first, then runs a filtered vector store search. Use when users have access to a small fraction of a large corpus.
- `create_check_permissions_node` — LangGraph node factory (renamed from `create_auth_node`). Post-filter: checks permissions on already-retrieved documents.
- `create_lookup_resources_node` — LangGraph node factory for pre-filter authorization. Calls `LookupResources`, runs a filtered vector search, and writes `authorized_documents` to state. No separate retrieval node needed.
- `SpiceDBAuthorizer.lookup_resources()` — underlying method wrapping SpiceDB's `LookupResources` streaming gRPC API.

### Removed

- `SpiceDBRetriever` — superseded by `SpiceDBAuthFilter` (more composable, supports runtime user injection).
- `SpiceDBAuthLambda` — superseded by `SpiceDBAuthFilter`.
- `fail_open` parameter — was silently granting full access on SpiceDB failures; errors now always propagate.

## [0.1.0] - 2026-04-01

Initial release.

- `SpiceDBAuthFilter` — post-filter authorization as a LangChain `Runnable`
- `SpiceDBPermissionTool` — single permission check for LangChain agents
- `SpiceDBBulkPermissionTool` — bulk permission checks for LangChain agents
- `create_auth_node` / `AuthorizationNode` — post-filter LangGraph nodes
- `RAGAuthState` — typed state for LangGraph RAG workflows
145 changes: 82 additions & 63 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ This package follows [LangChain's official integration guidelines](https://pytho

- **LangChain & LangGraph Integration**: First-class support for modern LLM frameworks
- **Vector Store Agnostic**: Compatible with Pinecone, FAISS, Weaviate, Chroma, and more
- **Post-Filter Authorization**: Filters retrieved documents based on SpiceDB permissions
- **Post-Filter Authorization**: Retrieve semantically, then filter by SpiceDB permissions
- **Pre-Filter Authorization**: Fetch authorized resource IDs via LookupResources first, then run a filtered vector store search — ideal when users have access to a small fraction of a large corpus
- **Efficient Bulk Permissions**: Uses SpiceDB's native bulk API for optimal performance
- **Observable**: Returns detailed metrics about authorization decisions
- **Type-Safe**: Full type hints for better IDE support
Expand All @@ -19,75 +20,76 @@ This package follows [LangChain's official integration guidelines](https://pytho
Most RAG pipelines retrieve documents without considering user permissions. This package solves that by:

1. **Post-retrieval filtering**: Retrieve best semantic matches first, then filter by permissions
2. **Deterministic authorization**: Every document is checked against SpiceDB before being used
3. **Framework integration**: Native LangChain and LangGraph components for seamless integration
4. **Vector store agnostic**: Not tied to any specific vector database
2. **Pre-retrieval filtering**: Fetch all resource IDs the user can access via SpiceDB's `LookupResources` API, then run a filtered vector store search — no unauthorized documents are retrieved
3. **Deterministic authorization**: Every document is checked against SpiceDB before being used
4. **Framework integration**: Native LangChain and LangGraph components for seamless integration
5. **Vector store agnostic**: Not tied to any specific vector database

## Which Component Should I Use?

Choose the right component based on your use case:

| Component | Use Case | Best For |
|-----------|----------|----------|
| **SpiceDBRetriever** | Simple RAG pipelines | Drop-in replacement for any retriever. Wraps your existing retriever with authorization. |
| **SpiceDBAuthFilter** | LangChain chains with middleware | Filtering documents in the middle of a chain. Reusable across different users via `config`. |
| **create_auth_node** | LangGraph workflows | Complex multi-step workflows with state management. Provides authorization metrics in state. |
| **SpiceDBPermissionTool** | Agentic workflows | Give agents the ability to check permissions before taking actions. |
| **SpiceDBBulkPermissionTool** | Agentic workflows (batch) | Same as above but for checking multiple resources at once. |
| Component | Pattern | Use Case |
|-----------|---------|----------|
| **SpiceDBAuthFilter** | Post-filter | LangChain LCEL chains. Reusable across different users via `config`. |
| **SpiceDBPreFilterRetriever** | Pre-filter | LangChain LCEL chains. Users have access to a small fraction of a large corpus. Requires a `filter_factory` matching your vector store's filter syntax. |
| **create_check_permissions_node** | Post-filter | LangGraph workflows. Multi-step workflows with state management and authorization metrics. |
| **create_lookup_resources_node** | Pre-filter | LangGraph workflows. Single node that calls LookupResources then runs a filtered vector search. No separate retrieval step needed. |
| **SpiceDBPermissionTool** | Check | Agents that need to check a single permission before acting. |
| **SpiceDBBulkPermissionTool** | Check | Agents that need to check permissions on multiple resources at once. |

### Quick Decision Guide

**Use SpiceDBRetriever if:**
- You have a simple RAG pipeline
- You always use the same user per retriever instance and you don't need to reuse the retriever across different users
**Post-filter vs Pre-filter:**
- Use **post-filter** (`SpiceDBAuthFilter`, `create_check_permissions_node`) when users have access to most documents — semantic search quality is highest because all documents are candidates.
- Use **pre-filter** (`SpiceDBPreFilterRetriever`, `create_lookup_resources_node`) when users have access to a small subset of a large corpus — avoids retrieving unauthorized content entirely.

**Use SpiceDBAuthFilter if:**
- You're building LangChain LCEL chains
- You want to reuse the same chain for multiple users
- You need to pass user context at runtime via `config`
**LangChain vs LangGraph:**
- Use **LangChain** components (`SpiceDBAuthFilter`, `SpiceDBPreFilterRetriever`) for LCEL chains.
- Use **LangGraph** components (`create_check_permissions_node`, `create_lookup_resources_node`) for state graph workflows.

**Use create_auth_node if:**
- You're using LangGraph for complex workflows
- You need state management and observability
- You're building multi-step agentic workflows
### Example: Same Pipeline, Different Patterns

**Use SpiceDBPermissionTool / SpiceDBBulkPermissionTool if:**
- You're building agents with LangChain
- Your agent needs to check permissions as part of its decision-making and you want agents to explain why actions are allowed or denied
- You're implementing permission-aware automation
**Pattern 1: SpiceDBAuthFilter (post-filter, LCEL)**
```python
auth = SpiceDBAuthFilter(...)
chain = retriever | auth | prompt | llm

### Example: Same Pipeline, Different Patterns
# Same chain, different users at call time
await chain.ainvoke("question", config={"configurable": {"subject_id": "alice"}})
await chain.ainvoke("question", config={"configurable": {"subject_id": "bob"}})
```

**Pattern 1: SpiceDBRetriever (simplest)**
**Pattern 2: SpiceDBPreFilterRetriever (pre-filter, LCEL)**
```python
retriever = SpiceDBRetriever(
base_retriever=vectorstore.as_retriever(),
subject_id="alice", # Fixed user
retriever = SpiceDBPreFilterRetriever(
vector_store=vector_store,
filter_factory=lambda ids: {"filter": {"article_id": {"$in": ids}}},
subject_id="alice",
...
)
chain = retriever | prompt | llm
```

**Pattern 2: SpiceDBAuthFilter (reusable)**
**Pattern 3: create_check_permissions_node (post-filter, LangGraph)**
```python
auth = SpiceDBAuthFilter(...)
chain = retriever | auth | prompt | llm

# Same chain, different users
await chain.ainvoke("question", config={"configurable": {"subject_id": "alice"}})
await chain.ainvoke("question", config={"configurable": {"subject_id": "bob"}})
graph.add_node("authorize", create_check_permissions_node(...))
# Authorization metrics available in state['auth_results']
```

**Pattern 3: LangGraph Node (stateful)**
**Pattern 4: create_lookup_resources_node (pre-filter, LangGraph)**
```python
graph.add_node("authorize", create_auth_node(...))
# Authorization metrics available in state['auth_results']
# Single node replaces separate retrieve + authorize nodes
graph.add_node("retrieve_authorized", create_lookup_resources_node(
vector_store=vector_store,
filter_factory=lambda ids: {"filter": {"article_id": {"$in": ids}}},
...
))
graph.add_edge("retrieve_authorized", "generate")
```

**Pattern 4: Agent Tool (agentic)**
**Pattern 5: Agent Tool**
```python
tools = [SpiceDBPermissionTool(...)]
agent = create_agent(llm, tools, system_prompt="You are a helpful assistant.")
agent = create_agent(llm, tools, system_prompt="...")
# Agent can check "Can user alice delete document 123?" and explain the result
```

Expand Down Expand Up @@ -155,7 +157,6 @@ from langchain_spicedb import SpiceDBAuthFilter
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

# Initialize auth filter
auth = SpiceDBAuthFilter(
spicedb_endpoint="localhost:50051",
spicedb_token="sometoken",
Expand All @@ -165,18 +166,16 @@ auth = SpiceDBAuthFilter(
permission="view",
)

# Build chain once
chain = (
RunnableParallel({
"context": retriever | auth, # Authorization happens here
"context": retriever | auth,
"question": RunnablePassthrough(),
})
| prompt
| llm
| StrOutputParser()
)

# Pass user at runtime - reuse same chain for different users
answer = await chain.ainvoke(
"Your question?",
config={"configurable": {"subject_id": "alice"}}
Expand All @@ -187,27 +186,24 @@ answer = await chain.ainvoke(

```python
from langgraph.graph import StateGraph, END
from langchain_spicedb import create_auth_node, RAGAuthState
from langchain_spicedb import create_check_permissions_node, RAGAuthState

graph = StateGraph(RAGAuthState)

# Add nodes
graph.add_node("retrieve", retrieve_node)
graph.add_node("authorize", create_auth_node(
graph.add_node("authorize", create_check_permissions_node(
spicedb_endpoint="localhost:50051",
spicedb_token="sometoken",
resource_type="article",
resource_id_key="article_id",
))
graph.add_node("generate", generate_node)

# Wire it up
graph.set_entry_point("retrieve")
graph.add_edge("retrieve", "authorize")
graph.add_edge("authorize", "generate")
graph.add_edge("generate", END)

# Run
app = graph.compile()
result = await app.ainvoke({
"question": "What is SpiceDB?",
Expand All @@ -224,28 +220,51 @@ result = await app.ainvoke({

## Components

### SpiceDBRetriever
### SpiceDBAuthFilter

Wraps any LangChain retriever with SpiceDB authorization:
Post-filter authorization as a LangChain `Runnable`. Sits between a retriever and the rest of the chain, filtering documents based on SpiceDB permissions. Reusable across users via `config`:

```python
from langchain_spicedb import SpiceDBRetriever
from langchain_spicedb import SpiceDBAuthFilter

retriever = SpiceDBRetriever(
base_retriever=vector_store.as_retriever(),
subject_id="alice",
auth = SpiceDBAuthFilter(
spicedb_endpoint="localhost:50051",
spicedb_token="sometoken",
resource_type="article",
subject_type="user",
resource_id_key="article_id",
permission="view",
)

authorized_docs = await auth.ainvoke(
docs,
config={"configurable": {"subject_id": "alice"}}
)
```

### SpiceDBPreFilterRetriever

Pre-filter authorization as a LangChain `BaseRetriever`. Calls SpiceDB's `LookupResources` to get the user's authorized IDs, then runs a filtered vector search:

```python
from langchain_spicedb import SpiceDBPreFilterRetriever

retriever = SpiceDBPreFilterRetriever(
vector_store=vector_store,
filter_factory=lambda ids: {"filter": {"article_id": {"$in": ids}}},
subject_id="alice",
spicedb_endpoint="localhost:50051",
spicedb_token="sometoken",
resource_type="article",
permission="view",
)

docs = await retriever.ainvoke("query")
docs = await retriever.ainvoke("What is Python?")
```

### SpiceDBPermissionTool

LangChain tool for agents to check permissions:
LangChain tool for agents to check a single permission:

```python
from langchain_spicedb import SpiceDBPermissionTool
Expand All @@ -267,7 +286,7 @@ result = tool.invoke({

### SpiceDBBulkPermissionTool

Same as `SpiceDBPermissionTool` but check permissions for multiple resources at once:
Same as `SpiceDBPermissionTool` but checks multiple resources at once:

```python
from langchain_spicedb import SpiceDBBulkPermissionTool
Expand Down
4 changes: 2 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,11 +337,11 @@ Metrics are automatically available in the state under `auth_results`:

```python
from langgraph.graph import StateGraph, END
from langchain_spicedb import create_auth_node, RAGAuthState
from langchain_spicedb import create_check_permissions_node, RAGAuthState

graph = StateGraph(RAGAuthState)
graph.add_node("retrieve", retrieve_node)
graph.add_node("authorize", create_auth_node(
graph.add_node("authorize", create_check_permissions_node(
spicedb_endpoint="localhost:50051",
spicedb_token="sometoken",
resource_type="article",
Expand Down
7 changes: 3 additions & 4 deletions docs/langchain-integration/spicedb.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,10 @@ The package provides several components for different integration patterns:
### Runnables (Middleware)

- **SpiceDBAuthFilter**: LCEL-compatible Runnable for authorization in chains
- **SpiceDBAuthLambda**: Lightweight wrapper for use with RunnableLambda

### LangGraph nodes

- **create_auth_node()**: Factory function to create authorization nodes
- **create_check_permissions_node()**: Factory function to create authorization nodes
- **AuthorizationNode**: Reusable class-based node for authorization
- **RAGAuthState**: TypedDict for LangGraph state with authorization fields

Expand Down Expand Up @@ -237,7 +236,7 @@ answer = await chain.ainvoke(

```python
from langgraph.graph import StateGraph, END
from langchain_spicedb import create_auth_node, RAGAuthState
from langchain_spicedb import create_check_permissions_node, RAGAuthState

# Define your nodes
def retrieve_node(state):
Expand All @@ -257,7 +256,7 @@ def generate_node(state):
# Build graph
graph = StateGraph(RAGAuthState)
graph.add_node("retrieve", retrieve_node)
graph.add_node("authorize", create_auth_node(
graph.add_node("authorize", create_check_permissions_node(
spicedb_endpoint="localhost:50051",
spicedb_token="sometoken",
resource_type="article",
Expand Down
Loading
Loading