Drop Stryda governance into a LangChain agent — wrap a BaseTool, or attach a callback to an AgentExecutor.
Project description
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file stryda_langchain-0.1.0.tar.gz.
File metadata
- Download URL: stryda_langchain-0.1.0.tar.gz
- Upload date:
- Size: 10.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
494b18b11d9695a04cdb19861e2112c785478af5246d9343b3b5e9ba628d1613
|
|
| MD5 |
6b4b12c5876b34fa3d80604f61b2d7b5
|
|
| BLAKE2b-256 |
f6e954bcdb51745d9b221430f12c4a36fd21fad3051d1e2f6431a693b3bba755
|
File details
Details for the file stryda_langchain-0.1.0-py3-none-any.whl.
File metadata
- Download URL: stryda_langchain-0.1.0-py3-none-any.whl
- Upload date:
- Size: 12.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
17263da219d344c9adef56582aba7ab4ba9665b06ea1a77cfcf38b222b980503
|
|
| MD5 |
1c276eaa3b5bb6dce10b4db499b12621
|
|
| BLAKE2b-256 |
5314a03b2569bd8b4dbc1ef851f000ed2a2e385139efab2fc231719b90c5893e
|