← Back to Build Log
langchainguardrailstutorialsecuritypolicy

Adding Safety Guardrails to Your LangChain Agent in 5 Minutes

LangChain makes it genuinely easy to build capable agents. You can wire up a language model with a set of tools in a dozen lines of code and have something that reads files, queries APIs, sends messages, and takes actions in the world. That ease is also the problem. Nothing in the default LangChain setup defines what the agent is allowed to do, under what conditions, or what happens when it does something unexpected. There is no policy layer. There is no audit trail. There is no kill switch.

You will discover this is a problem the first time your agent calls a production API it was never supposed to touch, or when your security team asks you to produce a record of every action the agent took last Tuesday.

This tutorial shows you how to add a full safety layer — policy enforcement, approval workflows, and cryptographic receipts — to a LangChain agent in about five minutes.

The Before State

Here is a standard LangChain agent setup. Nothing unusual about this code.

import { ChatOpenAI } from "@langchain/openai";
import { AgentExecutor, createOpenAIFunctionsAgent } from "langchain/agents";
import { DynamicTool } from "@langchain/core/tools";

const tools = [
  new DynamicTool({
    name: "send_email",
    description: "Send an email to a recipient",
    func: async (input) => {
      // sends email
      return `Email sent to ${input}`;
    },
  }),
  new DynamicTool({
    name: "delete_record",
    description: "Delete a database record by ID",
    func: async (id) => {
      // deletes record
      return `Record ${id} deleted`;
    },
  }),
];

const agent = await createOpenAIFunctionsAgent({
  llm: new ChatOpenAI({ model: "gpt-4o" }),
  tools,
  prompt,
});

const executor = new AgentExecutor({ agent, tools });

This agent can call send_email and delete_record without restriction. Any input, any target, any time. If the model hallucinates an argument or gets injected with a malicious instruction, there is nothing between the decision and the action.

Step 1: Install the Adapter

npm install @authensor/sdk @authensor/langchain

Set your API key:

export AUTHENSOR_API_KEY=your_api_key

Step 2: Write a Policy

Create policy.yaml in your project root. This is where you define what the agent is allowed to do.

version: "1"
default: deny

rules:
  - action: send_email
    principal: "agent:email-assistant"
    effect: allow
    conditions:
      - field: "args.recipient"
        operator: matches
        value: "@yourcompany.com$"
    rateLimit:
      window: "1h"
      max: 50

  - action: delete_record
    principal: "agent:email-assistant"
    effect: review
    reason: "Destructive actions require human approval"

  - action: "*"
    principal: "*"
    effect: deny

Three rules. send_email is allowed but only to your company domain and rate-limited to 50 per hour. delete_record requires a human to approve it before it executes. Everything else is denied by default.

Push the policy to your Authensor control plane:

npx authensor policy push policy.yaml

Step 3: Wrap Your Agent

Replace your AgentExecutor with the Authensor-wrapped version:

import { ChatOpenAI } from "@langchain/openai";
import { createOpenAIFunctionsAgent } from "langchain/agents";
import { createAuthensorExecutor } from "@authensor/langchain";

const agent = await createOpenAIFunctionsAgent({
  llm: new ChatOpenAI({ model: "gpt-4o" }),
  tools,
  prompt,
});

// Replace AgentExecutor with createAuthensorExecutor
const executor = createAuthensorExecutor({
  agent,
  tools,
  principal: "agent:email-assistant",
  apiKey: process.env.AUTHENSOR_API_KEY,
});

// Usage is identical
const result = await executor.invoke({ input: "Send a summary to alice@yourcompany.com" });

That is the entire change. createAuthensorExecutor wraps the tool-call boundary so that every time the agent decides to call a tool, the intent is evaluated against your policy before the tool function runs. The agent itself does not change. The tools do not change. The policy evaluation happens transparently between the two.

What Happens at Runtime

When the agent decides to call send_email:

  1. The adapter intercepts the tool call and sends an authorization/propose message to the control plane.
  2. The policy engine evaluates the intent: which principal is calling, which action, what arguments, against what rules.
  3. If the decision is ALLOW, the call is forwarded to your tool function. An immutable receipt is written.
  4. If the decision is DENY, the tool function is never called. A DENY receipt is written. The agent receives a structured error explaining why.
  5. If the decision is REVIEW, the intent is held and routed to your approval queue. The agent waits. When a human approves or rejects, the result is returned.

Common Policy Patterns

Block all destructive actions by default:

rules:
  - action: "delete_*"
    principal: "*"
    effect: deny

  - action: "drop_*"
    principal: "*"
    effect: deny

Allow reads, require approval for writes:

rules:
  - action: "read_*"
    principal: "agent:analyst"
    effect: allow

  - action: "write_*"
    principal: "agent:analyst"
    effect: review

Time-of-day restrictions:

rules:
  - action: "deploy_*"
    principal: "agent:ci-bot"
    effect: allow
    conditions:
      - field: "context.hour"
        operator: between
        value: [9, 17]
      - field: "context.dayOfWeek"
        operator: notIn
        value: [0, 6]

Per-action cost caps (for agents that call paid APIs):

rules:
  - action: "generate_image"
    principal: "agent:content-creator"
    effect: allow
    budget:
      perCall: 0.04
      daily: 5.00

The After State

The same agent code, with one import change and one constructor swap, now has:

  • Every tool call evaluated against a declarative policy before execution
  • Rate limiting enforced at the authorization layer, not scattered across tool implementations
  • Destructive actions held for human approval
  • An immutable, hash-chained audit trail of every decision
  • Prompt injection scanning on all tool inputs (if Aegis is enabled)

The policy lives in one file. It is version-controlled. You can push a new policy without redeploying the agent. If something goes wrong at 2am, you can deny all actions for a specific agent by pushing a single-rule policy that overrides everything.

Try It

The @authensor/langchain adapter is open source. Run npx create-authensor to scaffold a project with a policy, a receipt store, and an example LangChain integration. The full docs are at authensor.com/docs. If you find something that does not work the way it should, open an issue or star the repo so more people find it.