For: LangChain devs already shipping agents in production
Why LangChain needs SBO3L (and what changes when you wire them)
LangChain wraps your agent's reasoning. SBO3L wraps LangChain's tool calls in policy + signed audit. Five lines of code, one boundary your CFO understands.
You wrap your agent in LangChain. LangChain wraps your agent's reasoning: which tool to call, with which arguments, in what order. That's load-bearing. But LangChain doesn't wrap the boundary between "the agent decided" and "the action executed." That's the gap SBO3L closes.
The five-line wire
Vanilla LangChain — the agent reasons, the tool fires, no audit:
const chain = createOpenAIToolsAgent({ llm, tools, prompt });
const result = await chain.invoke({ input });
// → tool calls happened. Where's the receipt?
With @sbo3l/langchain — same chain, plus a callback handler:
import { Sbo3lCallbackHandler } from "@sbo3l/langchain";
const sbo3l = new Sbo3lCallbackHandler({
url: "http://localhost:8730",
agentId: "research-01",
onDeny: (reason) => logger.warn("policy deny:", reason),
});
const chain = createOpenAIToolsAgent({ llm, tools, prompt });
const result = await chain.invoke({ input }, { callbacks: [sbo3l] });
// → every tool call now produces a signed receipt
// → policy denies surface as a tool_result with deny_code
// → handler.receipts contains the full audit trail
What changes
- Every tool call has a signed receipt. Not a callback log. Not a database row your daemon writes after the fact. A cryptographic receipt produced before the tool executes, signed with the daemon's Ed25519 key, with the request hash and policy hash baked in.
- Policy denies are part of the chain output. LangChain's standard tool-call result protocol carries the deny code through to the LLM, so the agent can reason about rejection ("I can't transfer that much; ask the user to lower the amount") instead of crashing.
- The audit chain is queryable.
handler.receiptsis a list of all the receipts from this chain run. Hash-chained, exportable, replayable. Your SOC 2 auditor doesn't ask "did the agent do something it shouldn't?" — they ask "show me the receipts" and you ship the list.
Why your CFO wants this
Imagine the conversation when the LangChain-driven agent does something expensive (or wrong, or both):
| Without SBO3L | With SBO3L |
|---|---|
| "What happened?" — engineer reconstructs from logs, OpenAI traces, blockchain explorer, maybe LangSmith if they paid for it. Hours of forensic work. | "What happened?" — engineer pulls capsule by request_hash, verifies offline against the daemon's published Ed25519 pubkey, has cryptographic proof of what the agent decided and what policy was in force. Minutes. |
| "Could it have been worse?" — uncertain. Logs might be incomplete; the agent's reasoning chain is ephemeral. | "Could it have been worse?" — query the policy snapshot
referenced by policy_hash; show the rules that
prevented worse outcomes from firing. |
| "Who approved this?" — depends on which review you logged. Not always available. | "Who approved this?" — the receipt's
matched_rule_id points at the exact policy
rule. The rule's git history shows who shipped it. |
What this isn't
- Not a LangChain replacement. The handler is additive. Same agent, same tools, same prompts. SBO3L doesn't second-guess the LLM's reasoning — it just enforces the boundary around what the LLM can actually do.
- Not a free latency lunch. Each tool call adds one round-trip to the daemon (typically <1ms over Unix socket, ~5ms over HTTP). For human-perceived latency this is invisible; for high-frequency batch agents you can run the daemon in-process via the Rust crate directly.
- Not a substitute for prompt engineering. SBO3L can't stop the LLM from trying to call the wrong tool — but it can stop the call from succeeding. The LLM gets a deny code; your prompt should teach it to handle that gracefully.
Try it
The Node.js + Python LangChain adapter quickstart is at
/quickstart/langchain
— five minutes from npm install to your first
signed receipt against a local daemon.