stryda-langchain
Drop Stryda governance into a LangChain agent with ~3 lines of wiring.
This package is the LangChain-native surface on top of stryda-sdk. It ships two adapters — a tool wrapper and a callback handler — so you can pick whichever matches your existing agent structure. Both paths funnel every tool call through Stryda's stryda.check_action → execute → stryda.record_outcome pipeline.
Install
Not published to PyPI yet. Install from this monorepo:
pip install -e ./packages/stryda-sdk-python
pip install -e ./packages/stryda-langchain
Requires Python 3.10+ and langchain-core >= 0.3.
Option A — StrydaToolWrapper (recommended)
Wrap any BaseTool. The LLM sees the same tool (same name, description, args_schema), but every invocation goes through Stryda first. On deny/escalate the agent sees a readable policy reason as the tool output and can course-correct.
from langchain.agents import create_openai_tools_agent, AgentExecutor
from langchain_core.tools import tool
from stryda_sdk import StrydaClient
from stryda_langchain import StrydaToolWrapper
stryda = StrydaClient(api_key=os.environ["STRYDA_API_KEY"])
@tool
def refund_charge(charge_id: str, amount_cents: int) -> dict:
"""Refund a Stripe charge."""
return stripe.Refund.create(charge=charge_id, amount=amount_cents)
# One-liner — the rest of your agent setup is unchanged.
governed_refund = StrydaToolWrapper(
tool=refund_charge,
action_type="payments.refund",
stryda_client=stryda,
)
executor = AgentExecutor(
agent=agent,
tools=[governed_refund, other_tools...],
)
What happens on each invocation:
| Stryda decision | Wrapped tool runs? | Agent sees |
|---|---|---|
allow |
Yes | Real tool result; record_outcome=success |
deny |
No | "[stryda:denied] refund_charge was not run. Reason: …" |
escalate |
No | "[stryda:pending_approval] … (escalation_id=esc_xyz)" |
| Stryda 5xx | No | StrydaError raised — fail closed by design |
| Tool raises | — | record_outcome=error, exception re-raised |
Option B — StrydaCallback
If you can't (or don't want to) rewrite your tool objects, attach the callback at the AgentExecutor level. It governs every tool call in the agent loop.
from stryda_langchain import StrydaCallback
executor = AgentExecutor(
agent=agent,
tools=tools, # unchanged — still BYO-key
callbacks=[StrydaCallback(
stryda_client=stryda,
action_type_map={
"refund_charge": "payments.refund",
"send_email": "comms.email_send",
"create_ticket": "crm.ticket_create",
},
)],
)
The callback has raise_error = True, so a DeniedError raised from on_tool_start propagates through LangChain's callback manager and the tool is never executed. Unmapped tool names fall back to the tool's lowercased name coerced into Stryda's action_type regex.
Why only one class? Earlier versions of this package shipped a separate
AsyncStrydaCallbacksubclassingAsyncCallbackHandler. It had a silent correctness bug — when an agent usedainvokeon a sync tool, LangChain's dispatcher sometimes routed async handlers through_run_coros, which swallows exceptions unconditionally. Adenieddecision would then log-and-allow. The single sync-methods class avoids that path entirely. The importAsyncStrydaCallbackis still exported as an alias ofStrydaCallbackfor back-compat.
Offline attestation verification
Every authorized tool call produces a signed Ed25519 JWT. To verify stored attestations without hitting Stryda, use stryda-sdk's verifier:
from stryda_sdk import fetch_jwks, verify_attestation
jwks = fetch_jwks("https://api.stryda.ai") # cache per-process
claims = verify_attestation(jwt_token, jwks, audience="mcp-governance")
BYO-key stays BYO-key
Stryda never holds your Stripe / Gmail / Salesforce credentials. StrydaToolWrapper calls your original tool's _run with the original args; StrydaCallback lets LangChain execute your tool as it normally would. Stryda only authorizes and records. If your Stryda endpoint is unreachable, the wrapper fails closed (StrydaError) — it will never silently bypass governance.
Troubleshooting
Callback raised DeniedError but LangChain warned and ran the tool anyway.
You probably attached a handler that subclasses AsyncCallbackHandler with async def methods on a sync tool. Use StrydaCallback — it works in both sync and async paths.
Tool ran but no attestation in Stryda's ledger.
Check the check_id in your logs. record_outcome is best-effort (the action already happened); transient Stryda 5xx errors on record do NOT roll back the tool call. Your ledger-verify endpoint (GET /api/ledger/verify) will surface the gap if it drifted.
Wrapper is blocking LLM tool calling.
Make sure you passed tool= (not tool_cls=) a concrete instance of BaseTool, not a class. LangChain introspects args_schema on the instance.
Related
stryda-sdk-python— the underlying HTTP client +governedcontext managerstryda-langgraph— policy node for LangGraph state machines- Mission + architecture:
MISSION.md,docs/system-architecture.md
Metadata
Release files for stryda-langchain 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| stryda_langchain-0.1.0.tar.gz | 10.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| stryda_langchain-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 22.2 kB
Release files / stryda_langchain-0.1.0.tar.gz
| Download URL | stryda_langchain-0.1.0.tar.gz |
|---|---|
| Size | 10.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
494b18b11d9695a04cdb19861e2112c785478af5246d9343b3b5e9ba628d1613
|
|
BLAKE2b-256 checksum How to use checksums |
f6e954bcdb51745d9b221430f12c4a36fd21fad3051d1e2f6431a693b3bba755
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.9
|
Release files / stryda_langchain-0.1.0-py3-none-any.whl
| Download URL | stryda_langchain-0.1.0-py3-none-any.whl |
|---|---|
| Size | 12.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
17263da219d344c9adef56582aba7ab4ba9665b06ea1a77cfcf38b222b980503
|
|
BLAKE2b-256 checksum How to use checksums |
5314a03b2569bd8b4dbc1ef851f000ed2a2e385139efab2fc231719b90c5893e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.9
|