|
| 1 | +# hdp-langchain |
| 2 | + |
| 3 | +**HDP (Human Delegation Provenance) middleware for LangChain** — attach a cryptographic audit trail to any chain, agent, or tool with a single callback handler. |
| 4 | + |
| 5 | +Every tool call in a LangChain agent is recorded in a tamper-evident chain of Ed25519 signatures, verifiable offline with a single public key. |
| 6 | + |
| 7 | +``` |
| 8 | +pip install hdp-langchain |
| 9 | +``` |
| 10 | + |
| 11 | +--- |
| 12 | + |
| 13 | +## Quick start — LangChain |
| 14 | + |
| 15 | +```python |
| 16 | +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey |
| 17 | +from langchain_core.tools import tool |
| 18 | +from langchain.agents import AgentExecutor, create_react_agent |
| 19 | +from hdp_langchain import HdpMiddleware, HdpPrincipal, ScopePolicy, verify_chain |
| 20 | + |
| 21 | +# 1. Your signing key (store in a secrets manager, never in code) |
| 22 | +private_key = Ed25519PrivateKey.generate() |
| 23 | + |
| 24 | +# 2. Define what the human is authorising |
| 25 | +scope = ScopePolicy( |
| 26 | + intent="Research agent to summarise recent papers", |
| 27 | + authorized_tools=["web_search", "file_reader"], |
| 28 | + max_hops=10, |
| 29 | +) |
| 30 | + |
| 31 | +# 3. Create the middleware |
| 32 | +middleware = HdpMiddleware( |
| 33 | + signing_key=private_key.private_bytes_raw(), |
| 34 | + session_id="research-2026-q1", |
| 35 | + principal=HdpPrincipal(id="researcher@lab.edu", id_type="email"), |
| 36 | + scope=scope, |
| 37 | +) |
| 38 | + |
| 39 | +# 4. Build your agent as normal |
| 40 | +agent_executor = AgentExecutor(agent=..., tools=[...]) |
| 41 | + |
| 42 | +# 5. Attach HDP — one line, zero agent changes |
| 43 | +handler = middleware.get_callback_handler() |
| 44 | +agent_executor.invoke( |
| 45 | + {"input": "Summarise recent LLM papers"}, |
| 46 | + config={"callbacks": [handler]}, |
| 47 | +) |
| 48 | + |
| 49 | +# 6. Verify the delegation chain offline |
| 50 | +result = verify_chain(middleware.export_token(), private_key.public_key()) |
| 51 | +print(result.valid, result.hop_count, result.violations) |
| 52 | +``` |
| 53 | + |
| 54 | +--- |
| 55 | + |
| 56 | +## Quick start — LangGraph |
| 57 | + |
| 58 | +```python |
| 59 | +from langgraph.graph import StateGraph, END |
| 60 | +from hdp_langchain import HdpMiddleware, HdpPrincipal, ScopePolicy, verify_chain |
| 61 | +from hdp_langchain.graph import hdp_node |
| 62 | + |
| 63 | +middleware = HdpMiddleware( |
| 64 | + signing_key=private_key.private_bytes_raw(), |
| 65 | + session_id="graph-session-1", |
| 66 | + principal=HdpPrincipal(id="user@example.com", id_type="email"), |
| 67 | + scope=ScopePolicy(intent="Multi-node research pipeline"), |
| 68 | +) |
| 69 | + |
| 70 | +# Wrap node functions — each execution records a delegation hop |
| 71 | +@hdp_node(middleware, agent_id="planner") |
| 72 | +def planner_node(state): |
| 73 | + return {**state, "plan": "step 1, step 2"} |
| 74 | + |
| 75 | +@hdp_node(middleware, agent_id="executor") |
| 76 | +def executor_node(state): |
| 77 | + return {**state, "result": "done"} |
| 78 | + |
| 79 | +# Build and run the graph |
| 80 | +graph = StateGraph(dict) |
| 81 | +graph.add_node("planner", planner_node) |
| 82 | +graph.add_node("executor", executor_node) |
| 83 | +graph.add_edge("planner", "executor") |
| 84 | +graph.add_edge("executor", END) |
| 85 | +graph.set_entry_point("planner") |
| 86 | + |
| 87 | +app = graph.compile() |
| 88 | +app.invoke({}) |
| 89 | + |
| 90 | +# Verify the full delegation chain |
| 91 | +result = verify_chain(middleware.export_token(), private_key.public_key()) |
| 92 | +print(result.valid, result.hop_count) # True, 2 |
| 93 | +``` |
| 94 | + |
| 95 | +--- |
| 96 | + |
| 97 | +## Five design considerations |
| 98 | + |
| 99 | +| # | Consideration | How it's handled | |
| 100 | +|---|---|---| |
| 101 | +| **1** | **Scope enforcement** | Tool calls are checked against `authorized_tools` in `on_tool_start`. Default: logs + records violation in token. `strict=True`: raises `HDPScopeViolationError`. | |
| 102 | +| **2** | **Delegation depth** | `ScopePolicy(max_hops=N)` enforced per run; hops beyond the limit are skipped and logged. | |
| 103 | +| **3** | **Token size / performance** | Ed25519 signatures are 64 bytes each (~2.6 KB for a 10-hop run). All HDP operations are non-blocking — failures log as warnings, execution always continues. | |
| 104 | +| **4** | **Verification** | `verify_chain(token, public_key)` validates root + every hop signature offline. Returns `VerificationResult` with `valid`, `hop_count`, `violations`, and per-hop outcomes. | |
| 105 | +| **5** | **Callback integration** | `get_callback_handler()` returns an `HdpCallbackHandler` compatible with LangChain's `RunnableConfig`. For LangGraph, use `hdp_node()` to wrap node functions. | |
| 106 | + |
| 107 | +--- |
| 108 | + |
| 109 | +## API reference |
| 110 | + |
| 111 | +### `HdpMiddleware` |
| 112 | + |
| 113 | +```python |
| 114 | +HdpMiddleware( |
| 115 | + signing_key: bytes, # Ed25519 private key (raw 32 bytes) |
| 116 | + session_id: str, # unique ID for this run |
| 117 | + principal: HdpPrincipal, # the human delegating authority |
| 118 | + scope: ScopePolicy, # what is authorised |
| 119 | + key_id: str = "default", # label stored in the token header |
| 120 | + expires_in_ms: int = 86400000, |
| 121 | + strict: bool = False, # True → raise on scope violations |
| 122 | +) |
| 123 | +``` |
| 124 | + |
| 125 | +| Method | Description | |
| 126 | +|---|---| |
| 127 | +| `get_callback_handler()` | Return an `HdpCallbackHandler` for use with LangChain's `RunnableConfig` | |
| 128 | +| `export_token()` | Return the token dict (or `None` before first run) | |
| 129 | +| `export_token_json()` | Return the token as a JSON string | |
| 130 | + |
| 131 | +### `HdpCallbackHandler` |
| 132 | + |
| 133 | +A `langchain_core.callbacks.BaseCallbackHandler` subclass. Attach via `RunnableConfig`: |
| 134 | + |
| 135 | +```python |
| 136 | +config = {"callbacks": [middleware.get_callback_handler()]} |
| 137 | +chain.invoke(input, config=config) |
| 138 | +``` |
| 139 | + |
| 140 | +### `hdp_node(middleware, node_fn=None, *, agent_id=None)` |
| 141 | + |
| 142 | +Wraps a LangGraph node function to record a delegation hop on each invocation: |
| 143 | + |
| 144 | +```python |
| 145 | +@hdp_node(middleware, agent_id="researcher") |
| 146 | +def researcher_node(state): |
| 147 | + ... |
| 148 | + return state |
| 149 | +``` |
| 150 | + |
| 151 | +### `verify_chain(token, public_key)` |
| 152 | + |
| 153 | +```python |
| 154 | +result = verify_chain(token_dict, public_key) # Ed25519PublicKey or raw bytes |
| 155 | +result.valid # bool |
| 156 | +result.hop_count # int |
| 157 | +result.violations # list[str] |
| 158 | +result.hop_results # list[HopVerification] |
| 159 | +``` |
| 160 | + |
| 161 | +### `ScopePolicy` |
| 162 | + |
| 163 | +```python |
| 164 | +ScopePolicy( |
| 165 | + intent: str, |
| 166 | + data_classification: str = "internal", # "public" | "internal" | "confidential" | "restricted" |
| 167 | + network_egress: bool = True, |
| 168 | + persistence: bool = False, |
| 169 | + authorized_tools: list[str] | None = None, |
| 170 | + authorized_resources: list[str] | None = None, |
| 171 | + max_hops: int | None = None, |
| 172 | +) |
| 173 | +``` |
| 174 | + |
| 175 | +--- |
| 176 | + |
| 177 | +## Error handling |
| 178 | + |
| 179 | +By default, HDP middleware is **non-blocking** — signing or scope-check failures are logged as warnings and execution continues normally. Violations are recorded in the token for post-hoc audit. |
| 180 | + |
| 181 | +```python |
| 182 | +# Default (non-blocking): violations are logged, execution keeps running |
| 183 | +middleware = HdpMiddleware( |
| 184 | + signing_key=key, session_id="s1", |
| 185 | + principal=HdpPrincipal(id="alice", id_type="handle"), |
| 186 | + scope=ScopePolicy(intent="research", authorized_tools=["web_search"]), |
| 187 | +) |
| 188 | +handler = middleware.get_callback_handler() |
| 189 | +# If the agent calls an unauthorised tool (e.g. "execute_code"): |
| 190 | +# → WARNING is logged, violation attached to the token |
| 191 | +# → execution is NOT interrupted |
| 192 | + |
| 193 | +# Strict mode: violations raise immediately |
| 194 | +middleware_strict = HdpMiddleware( |
| 195 | + ..., |
| 196 | + strict=True, |
| 197 | +) |
| 198 | +# If the agent calls "execute_code" → raises HDPScopeViolationError |
| 199 | +``` |
| 200 | + |
| 201 | +--- |
| 202 | + |
| 203 | +## Cross-language compatibility |
| 204 | + |
| 205 | +Python and TypeScript HDP tokens use the same wire format (RFC 8785 canonical JSON + Ed25519). A token issued by `hdp-langchain` (Python) can be verified by `@helixar_ai/hdp` (TypeScript) and vice versa. |
| 206 | + |
| 207 | +```python |
| 208 | +# Python: export token |
| 209 | +token_json = middleware.export_token_json() |
| 210 | +# → pass to TypeScript service via API, message queue, etc. |
| 211 | +``` |
| 212 | + |
| 213 | +```typescript |
| 214 | +// TypeScript: verify a token issued by Python |
| 215 | +import { verifyChain } from "@helixar_ai/hdp"; |
| 216 | +const result = verifyChain(JSON.parse(tokenJson), publicKey); |
| 217 | +``` |
| 218 | + |
| 219 | +--- |
| 220 | + |
| 221 | +## Spec |
| 222 | + |
| 223 | +Human Delegation Provenance (HDP) is an IETF draft: |
| 224 | +[draft-helixar-hdp-agentic-delegation](https://datatracker.ietf.org/doc/draft-helixar-hdp-agentic-delegation/) |
| 225 | + |
| 226 | +## License |
| 227 | + |
| 228 | +[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) — Helixar Limited |
0 commit comments