@decionis/langchain
v0.1.0
Published
Gate any LangChain.js tool or LangGraph.js node on a signed Decionis Decision Dossier.
Maintainers
Readme
@decionis/langchain
Gate any LangChain.js tool call or LangGraph.js node on a signed Decionis Decision Dossier. The agent picks the tool; Decionis decides whether the call is allowed to fire — and records every verdict as a verifiable proof artifact.
npm install @decionis/langchain @decionis/sdk @langchain/coreMirrors decionis-langchain
for Python: same shadow → enforce rollout, same ?source=langchain_agent
verify-URL attribution, same per-tool short-circuit semantics.
Why
LangChain agents can be jailbroken, prompt-injected, or hallucinated into firing tools that move money, change pricing, send refunds, or delete data. Wrapping the tool with Decionis means:
- Every tool invocation gets a signed Decision Dossier — the policy
verdict (
APPROVE/REJECT/REVIEW/ESCALATE), the agent identity, the call arguments, and a public verify URL. - Blocked calls short-circuit before the inner tool runs — the LLM sees a
structured refusal (
DecionisGateRefusal) carrying the dossier id; the caller's audit log keeps the proof. - Shadow mode records verdicts without blocking, so a team can roll out policy gradually and review the would-have-rejected rate before enforcing.
Quick start — wrap a LangChain.js tool
import { createDecionisNodeSdk } from "@decionis/sdk";
import { DecionisGateTool } from "@decionis/langchain";
import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";
const sendRefund = new DynamicStructuredTool({
name: "send_refund",
description: "Issue a refund. Idempotent on customer_id + amount + day.",
schema: z.object({ customer_id: z.string(), amount_usd: z.number() }),
func: async ({ customer_id, amount_usd }) => "refund_id-abc",
});
const decionis = createDecionisNodeSdk({
baseUrl: "https://api.decionis.com",
apiKey: process.env.DECIONIS_API_KEY!,
});
const gatedRefund = DecionisGateTool.wrap({
innerTool: sendRefund,
client: decionis,
orgId: process.env.DECIONIS_ORG_ID!,
decisionType: "refund_execution",
siteBaseUrl: "https://decionis.com",
});
await agent.bindTools([gatedRefund]);When the LLM picks send_refund, Decionis evaluates first. On APPROVE the
inner tool runs and the result is returned. On REJECT / REVIEW /
ESCALATE (configurable) the wrapper throws DecionisGateRefusal carrying
the dossier id, reason codes, and a public verify URL the LLM can read and
the operator can forward.
Quick start — LangGraph.js node
import { StateGraph, END } from "@langchain/langgraph";
import { decionisGateNode } from "@decionis/langchain";
const graph = new StateGraph<MyState>({
/* channels */
});
graph.addNode("plan", planNode);
graph.addNode(
"gate",
decionisGateNode({
client: decionis,
orgId: process.env.DECIONIS_ORG_ID!,
decisionType: "refund_execution",
siteBaseUrl: "https://decionis.com",
extractCall: (state) => ({
toolName: state.proposedTool as string,
toolArgs: state.proposedArgs as Record<string, unknown>,
}),
}),
);
graph.addNode("execute", executeNode);
graph.addNode("refuse", refuseNode);
graph.addEdge("plan", "gate");
graph.addConditionalEdges("gate", (s) => (s.decionis as { outcome: string }).outcome, {
allowed: "execute",
blocked: "refuse",
});
graph.addEdge("execute", END);
graph.addEdge("refuse", END);The node writes a JSON-serializable record under state.decionis so the
graph stays checkpointable and the conditional edge can branch on allowed
or blocked.
Shadow-mode rollout
Same pattern as the Python wrapper and the GitHub Action: ship in shadow first, review the verdict distribution, then flip to enforce.
The end-to-end PLG funnel — pick a surface → install in shadow → watch verdicts → flip — is walked at decionis.com/shadow-mode?surface=langchain_js.
DecionisGateTool.wrap({
innerTool: sendRefund,
client: decionis,
orgId,
decisionType: "refund_execution",
shadowMode: true, // ← every verdict recorded; inner tool always runs
});In shadow mode the gate never throws — even on REJECT — so existing
agent behaviour is unchanged. Verdicts still flow into the dossier ledger so
the rollout team can grade policy fit before enforcing.
Tunables
| Option | Default | Purpose |
| --------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| client | required | DecionisDecisionExecutor from createDecionisNodeSdk(...). |
| orgId | required | Decionis org id (UUID). |
| decisionType | required | Canonical decision type — usually the workflow key (e.g. refund_execution). |
| workflowKey | decisionType | Override when the workflow key needs to differ from the decision type. |
| blockOutcomes | ["REJECT","REVIEW","ESCALATE"] | Outcomes that short-circuit the wrapper. Pass ["REJECT"] to let review verdicts through with the dossier recorded. |
| shadowMode | false | Record verdicts but never throw / never route to blocked. |
| siteBaseUrl | undefined | Base URL used to build a public verify URL (/verify/decision-dossiers/<id>?source=langchain_agent&sig=<dossier_sha256>). |
| actor | { type: "ai_agent", framework: "langchain_js" } | Extra actor metadata (model, session id, user id) merged into the dossier context. |
| onDecision | undefined | Observer callback (GateResult) => void. Exceptions inside it never break the gate. |
Honesty notes
shadowMode: trueis the only switch that lets the inner tool run on a blocking verdict; the default never silently passes aREJECT. Locked by tests.DecionisGateRefusal.verifyUrlis the same artifact link Slack / Teams / LinkedIn unfurl with the OG card from/api/og/verify/[id]. Forward it to a reviewer when you want the proof one click away.- The wrapper preserves the inner tool's name, description, and args schema unchanged so the LLM's tool selection behaviour does not drift.
- The observer callback's exceptions are swallowed so telemetry can never take down policy enforcement.
Compatibility
- Node ≥ 20
@decionis/sdk(workspace)@langchain/core≥ 0.3 (peer)@langchain/langgraphis not required to usedecionisGateNode— the factory returns a plainstate => Promise<state>callable, so LangGraph stays an optional runtime dependency.
