Skip to main content
LangChain makes it easy to give an agent a list of tools and let the LLM decide which ones to call. That is the feature. It is also the problem. When you hand a LangChain agent tools for create_lead, delete_contact, run_payroll, and search_emails, the agent can call any of them. There is no built-in mechanism to say “this user only approved read access” or “this agent should never be able to delete anything.” The LLM picks the tool based on its reasoning, and whatever it picks, runs. This post shows how to add real permissions to LangChain agent tools — scoped, per-user, revocable, and enforced before execution.

The Problem: LangChain Tools Have No Access Control

Here is a typical LangChain agent setup:
import { ChatOpenAI } from '@langchain/openai';
import { createToolCallingAgent, AgentExecutor } from 'langchain/agents';
import { DynamicTool } from '@langchain/core/tools';

const tools = [
  new DynamicTool({
    name: 'search_contacts',
    description: 'Search CRM contacts',
    func: async (input) => searchContacts(input),
  }),
  new DynamicTool({
    name: 'delete_contact',
    description: 'Delete a CRM contact by ID',
    func: async (input) => deleteContact(input),
  }),
];

const agent = createToolCallingAgent({ llm, tools, prompt });
const executor = new AgentExecutor({ agent, tools });
Both tools are equally available to the agent. If the user says “clean up duplicate contacts,” the agent may call delete_contact repeatedly. There is no permission check, no scope enforcement, and no audit trail. Three specific gaps:
  1. No per-user permissions. Every user gets the same tools with the same access level. You cannot give User A read-only access and User B write access without building custom middleware.
  2. No runtime enforcement. Once a tool is in the tools array, it is callable. There is no check at invocation time to verify the caller has permission.
  3. No audit logging. LangChain does not record which tools were called, by which agent, on behalf of which user, or whether the call was authorized.

The Solution: Scope-Enforced LangChain Tools

Grantex adds per-tool scope enforcement to LangChain agents. The core idea: every tool call is checked against the user’s approved scopes before execution. If the token does not include the required scope, the tool rejects the call immediately — the underlying function never runs. There are two approaches, depending on how much control you want.

Approach 1: createGrantexTool — Per-Tool Scope Wrapping

The @grantex/langchain package provides createGrantexTool, which creates a LangChain DynamicTool with a scope check baked in:
import { createGrantexTool } from '@grantex/langchain';

const searchTool = createGrantexTool({
  name: 'search_contacts',
  description: 'Search CRM contacts',
  grantToken,                          // JWT from Grantex token exchange
  requiredScope: 'contacts:read',      // must be in token's scp claim
  func: async (input) => {
    return JSON.stringify(await searchContacts(input));
  },
});

const deleteTool = createGrantexTool({
  name: 'delete_contact',
  description: 'Delete a CRM contact by ID',
  grantToken,
  requiredScope: 'contacts:delete',
  func: async (input) => {
    return JSON.stringify(await deleteContact(input));
  },
});

const agent = createToolCallingAgent({
  llm,
  tools: [searchTool, deleteTool],
  prompt,
});
The scope check is offline — it reads the scp claim from the JWT directly, no network call needed. If a user’s grant token only includes contacts:read, calling delete_contact throws a PermissionError before your function executes.

Approach 2: wrapTool with Manifests — Manifest-Based Enforcement

For larger agents with many tools, defining scopes inline gets repetitive. Grantex manifests let you define all tool-to-permission mappings in one place and wrap existing LangChain tools:
import { Grantex, ToolManifest, Permission } from '@grantex/sdk';

const gx = new Grantex({ apiKey: process.env.GRANTEX_API_KEY });

// Define a manifest for your CRM tools
gx.loadManifest(new ToolManifest({
  connector: 'crm',
  description: 'CRM integration tools',
  tools: {
    'search_contacts':  Permission.READ,
    'create_contact':   Permission.WRITE,
    'update_contact':   Permission.WRITE,
    'delete_contact':   Permission.DELETE,
    'export_all':       Permission.ADMIN,
  },
}));

// Wrap existing LangChain tools — enforcement is automatic
const protectedSearch = gx.wrapTool(searchContactsTool, {
  connector: 'crm',
  tool: 'search_contacts',
  grantToken: () => currentGrantToken,
});

const protectedDelete = gx.wrapTool(deleteContactTool, {
  connector: 'crm',
  tool: 'delete_contact',
  grantToken: () => currentGrantToken,
});
Grantex ships 53 pre-built manifests for popular services. If your agent uses Salesforce, HubSpot, Jira, or Stripe tools, you can skip the manifest definition entirely:
import { salesforceManifest } from '@grantex/sdk/manifests/salesforce';
import { jiraManifest } from '@grantex/sdk/manifests/jira';

gx.loadManifests([salesforceManifest, jiraManifest]);
Browse all available manifests with grantex manifest list from the CLI.

Adding Audit Logging to LangChain Agents

Scope enforcement blocks unauthorized calls. Audit logging records authorized ones. The GrantexAuditHandler is a LangChain callback that automatically logs every tool invocation to the Grantex audit trail:
import { Grantex } from '@grantex/sdk';
import { GrantexAuditHandler } from '@grantex/langchain';

const client = new Grantex({ apiKey: process.env.GRANTEX_API_KEY });

const auditHandler = new GrantexAuditHandler({
  client,
  agentId: 'ag_01ABC...',
  agentDid: 'did:key:z6Mk...',
  principalId: 'user_01',
  grantToken,
});

const result = await executor.invoke(
  { input: 'Find all contacts in New York' },
  { callbacks: [auditHandler] },
);
Each tool call logs the tool name, success/failure status, the agent identity, and the user who authorized the action. You can query the audit trail later via gx.audit.list() or stream events in real time via gx.events.stream().

Custom Manifests for Custom Tools

Most production agents use a mix of SaaS connectors and internal APIs. You can define custom manifests for any tool:
import { ToolManifest, Permission } from '@grantex/sdk';

gx.loadManifest(new ToolManifest({
  connector: 'internal-billing',
  description: 'Internal billing system',
  tools: {
    'get_invoice':       Permission.READ,
    'create_invoice':    Permission.WRITE,
    'void_invoice':      Permission.DELETE,
    'run_period_close':  Permission.ADMIN,
  },
}));
Or generate a manifest from your source code using the CLI:
grantex manifest generate agent_tools.py --out billing-manifest.json
Custom and pre-built manifests work identically at runtime. The permission hierarchy (read < write < delete < admin) applies to both.

The Permission Hierarchy

Grantex uses a four-level permission hierarchy. Higher levels subsume lower ones:
LevelPermissionAllowsExample Scopes
0readQuery, list, search, fetchtool:crm:read
1writeRead + create, update, sendtool:crm:write
2deleteRead + write + delete, removetool:crm:delete
3adminEverythingtool:crm:admin
A user granted tool:crm:write can use both search_contacts (read) and create_contact (write), but not delete_contact (delete) or export_all (admin). This is enforced automatically — you do not need to implement the hierarchy logic yourself.

Putting It All Together

Here is a complete example: a LangChain agent with scoped tools, manifest enforcement, and audit logging:
import { ChatOpenAI } from '@langchain/openai';
import { createToolCallingAgent, AgentExecutor } from 'langchain/agents';
import { Grantex } from '@grantex/sdk';
import { salesforceManifest } from '@grantex/sdk/manifests/salesforce';
import { createGrantexTool } from '@grantex/langchain';
import { GrantexAuditHandler } from '@grantex/langchain';

// 1. Initialize Grantex with the Salesforce manifest
const gx = new Grantex({ apiKey: process.env.GRANTEX_API_KEY });
gx.loadManifest(salesforceManifest);

// 2. Create scope-enforced tools
const tools = [
  createGrantexTool({
    name: 'search_contacts',
    description: 'Search Salesforce contacts',
    grantToken: userGrantToken,
    requiredScope: 'tool:salesforce:read',
    func: async (input) => JSON.stringify(await sfSearch(input)),
  }),
  createGrantexTool({
    name: 'create_lead',
    description: 'Create a new Salesforce lead',
    grantToken: userGrantToken,
    requiredScope: 'tool:salesforce:write',
    func: async (input) => JSON.stringify(await sfCreateLead(input)),
  }),
];

// 3. Create the agent with audit logging
const llm = new ChatOpenAI({ model: 'gpt-4o' });
const agent = createToolCallingAgent({ llm, tools, prompt });
const executor = new AgentExecutor({ agent, tools });

const auditHandler = new GrantexAuditHandler({
  client: gx,
  agentId: 'ag_salesforce_bot',
  agentDid: 'did:grantex:ag_salesforce_bot',
  principalId: 'user_alice',
  grantToken: userGrantToken,
});

// 4. Run — tools are scope-checked, actions are audit-logged
const result = await executor.invoke(
  { input: 'Find contacts in San Francisco and create a lead for the top one' },
  { callbacks: [auditHandler] },
);
If userGrantToken only contains tool:salesforce:read, the create_lead call is blocked before execution. The audit trail records both the successful search and the denied creation attempt.

Next Steps