← Back to blog
· By

Securing LangChain Agent Tools with Kakunin

Step-by-step tutorial: add cryptographic scope enforcement to LangChain tool calls using KakuninToolGuard. Block scope violations before execution.

Securing LangChain Agent Tools with Kakunin
Table of Contents

LangChain's tool abstraction makes it easy to give an AI agent access to external capabilities — web search, database queries, trade execution, file operations. That same abstraction is also the place where scope violations happen: an agent that should only read data ends up writing it, or a tool scoped to test data runs against production.

This tutorial shows how to add cryptographic scope enforcement to LangChain tool calls using the Kakunin Python SDK, so that every tool execution is gated on a verified X.509 certificate.

What You Will Build

By the end of this tutorial, your LangChain agent will:

  • Verify its X.509 certificate is active before executing any tool
  • Enforce that the agent holds the required scope for each specific tool
  • Raise a ScopeViolationError — not silently fail — if the check does not pass
  • Log every tool execution as a behavioural event for compliance audit

Prerequisites

  • Python 3.11+
  • A Kakunin account with an API key (kak_live_...)
  • A registered and certified agent (see the quickstart)
  • LangChain installed: pip install langchain langchain-openai

Step 1: Install the Kakunin SDK

pip install kakunin

Set your environment variables:

export KKN_API_KEY=kak_live_...
export KKN_AGENT_ID=agt_... # from your Kakunin dashboard
export OPENAI_API_KEY=sk-...

Step 2: Understand the Scope Model

Every Kakunin-certified agent carries its permitted scopes in its metadata. When you register an agent, you define what it is authorised to do:

import asyncio
import kakunin

kkn = kakunin.Kakunin(api_key="kak_live_...")

async def register():
agent = await kkn.agents.create(
name="Trading Research Agent",
model="gpt-4o",
version="v1.0.0",
model_hash=kakunin.Kakunin.compute_model_hash("gpt-4o"),
metadata={
"scopes": ["market_data:read", "research:write", "portfolio:read"]
# No "trades:execute" — this agent is read-only
}
)
cert = await kkn.agents.certify(agent.id)
print(f"Agent {agent.id} certified: {cert.serial_number}")

asyncio.run(register())

The scopes list in metadata defines what the agent is authorised to do. verify_agent_scope checks this list before every tool execution.

Step 3: Wrap a LangChain Tool with KakuninToolGuard

KakuninToolGuard wraps any LangChain BaseTool and adds a pre-execution scope check. If the agent does not hold the required scope, execution is blocked before the tool's _run method is ever called.

import os
from langchain.tools import BaseTool
from kakunin.integrations.langchain import KakuninToolGuard
import kakunin

kkn = kakunin.Kakunin(api_key=os.environ["KKN_API_KEY"])
AGENT_ID = os.environ["KKN_AGENT_ID"]

# Define your underlying tool
class MarketDataTool(BaseTool):
name: str = "get_market_data"
description: str = "Fetch current price and volume data for a given ticker symbol."

def _run(self, ticker: str) -> str:
# Your actual market data integration here
return f"AAPL: $189.42, Vol: 54.2M"

async def _arun(self, ticker: str) -> str:
return self._run(ticker)

# Wrap it — requires "market_data:read" scope
market_data_tool = KakuninToolGuard(
tool=MarketDataTool(),
kakunin=kkn,
agent_id=AGENT_ID,
required_scopes=["market_data:read"],
)

Now market_data_tool behaves like a standard LangChain tool — you pass it to your agent exactly the same way — but every invocation first verifies the agent's certificate.

Step 4: Define a Restricted Tool

Some tools should only be available to agents with elevated scope. Here is a trade execution tool that requires trades:execute:

class TradeExecutionTool(BaseTool):
name: str = "execute_trade"
description: str = "Execute a market order for the given ticker and quantity."

def _run(self, ticker: str, quantity: int) -> str:
# Real execution logic here
return f"Order submitted: BUY {quantity} {ticker}"

async def _arun(self, ticker: str, quantity: int) -> str:
return self._run(ticker, quantity)

# Wrap with scope enforcement — requires "trades:execute"
trade_tool = KakuninToolGuard(
tool=TradeExecutionTool(),
kakunin=kkn,
agent_id=AGENT_ID,
required_scopes=["trades:execute"],
)

When the research agent (which has ["market_data:read", "research:write", "portfolio:read"]) tries to call execute_trade, it will receive:

ScopeViolationError: Agent 'agt_...' missing required scopes: ['trades:execute']

The trade is never attempted.

Step 5: Build the Agent

Wire both tools into a standard LangChain agent:

from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from kakunin.exceptions import ScopeViolationError

llm = ChatOpenAI(model="gpt-4o", temperature=0)

prompt = ChatPromptTemplate.from_messages([
("system", "You are a trading research assistant. Gather market data and write research notes."),
MessagesPlaceholder("chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
])

# Only provide tools the agent is scoped for
# Including trade_tool here would be caught at runtime by KakuninToolGuard
tools = [market_data_tool]

agent = create_openai_tools_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

Step 6: Handle ScopeViolationError

ScopeViolationError carries structured context so you can handle it precisely:

from kakunin.exceptions import ScopeViolationError

try:
result = executor.invoke({"input": "What is the current price of AAPL?"})
print(result["output"])
except ScopeViolationError as e:
print(f"Scope violation: {e}")
print(f" Agent ID: {e.agent_id}")
print(f" Agent status: {e.agent_status}")
print(f" Missing scopes: {e.missing_scopes}")
# Log to your incident system, alert on-call, etc.

The error also fires if the agent's certificate has been revoked — e.agent_status will be "revoked" rather than "active". This means a compromised or non-compliant agent is blocked from all tool execution automatically, with no code change required.

Step 7: Add the Scope Callback for Full Chain Coverage

KakuninToolGuard protects individual tools. If you want a single check that gates the entire agent invocation before any tool is called, use langchain_scope_callback:

from kakunin.integrations.langchain import langchain_scope_callback

# Create a callback that checks scope before every LLM call
scope_cb = langchain_scope_callback(
kakunin=kkn,
agent_id=AGENT_ID,
required_scopes=["market_data:read"], # minimum scope for this chain
)

result = executor.invoke(
{"input": "Research AAPL and write a one-paragraph summary."},
config={"callbacks": [scope_cb]},
)

The callback raises ScopeViolationError on the first LLM call if the certificate is invalid — before any tokens are generated or any tools are invoked.

Step 8: Emit Behavioural Events for Compliance

Scope enforcement satisfies the EU AI Act Article 15 cybersecurity requirement. The Article 12 audit logging requirement needs a separate instrumentation step — emit a behavioural event for each tool execution:

import asyncio
from langchain.callbacks.base import BaseCallbackHandler

class KakuninAuditCallback(BaseCallbackHandler):
def __init__(self, kkn, agent_id):
self.kkn = kkn
self.agent_id = agent_id

def on_tool_end(self, output, **kwargs):
# Fire-and-forget — do not block the agent
asyncio.ensure_future(
self.kkn.events.ingest({
"agent_id": self.agent_id,
"action_type": "tool_call",
"metadata": {"output_length": len(str(output))},
})
)

audit_cb = KakuninAuditCallback(kkn, AGENT_ID)

result = executor.invoke(
{"input": "What is the current price of AAPL?"},
config={"callbacks": [scope_cb, audit_cb]},
)

Each event creates an immutable audit record stored in WORM-backed storage, satisfying the five-year retention requirement under EU AI Act Article 12.

Complete Working Example

import os
import asyncio
from langchain.tools import BaseTool
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from kakunin.integrations.langchain import KakuninToolGuard, langchain_scope_callback
from kakunin.exceptions import ScopeViolationError
import kakunin

kkn = kakunin.Kakunin(api_key=os.environ["KKN_API_KEY"])
AGENT_ID = os.environ["KKN_AGENT_ID"]

class MarketDataTool(BaseTool):
name: str = "get_market_data"
description: str = "Get current price and volume for a ticker."
def _run(self, ticker: str) -> str:
return f"{ticker}: $189.42, Vol: 54.2M"
async def _arun(self, ticker: str) -> str:
return self._run(ticker)

tools = [
KakuninToolGuard(
tool=MarketDataTool(),
kakunin=kkn,
agent_id=AGENT_ID,
required_scopes=["market_data:read"],
)
]

llm = ChatOpenAI(model="gpt-4o", temperature=0)
prompt = ChatPromptTemplate.from_messages([
("system", "You are a trading research assistant."),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
])

agent = create_openai_tools_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
scope_cb = langchain_scope_callback(kkn, AGENT_ID, required_scopes=["market_data:read"])

try:
result = executor.invoke(
{"input": "What is the current price of AAPL?"},
config={"callbacks": [scope_cb]},
)
print(result["output"])
except ScopeViolationError as e:
print(f"Blocked: {e.missing_scopes}")

What Happens When a Certificate Is Revoked

When Kakunin auto-revokes a certificate (risk score ≥ 0.85), the revocation propagates to the CDN within 60 seconds. On the next tool invocation, KakuninToolGuard calls kkn.agents.get(agent_id) and receives status: "revoked". The ScopeViolationError is raised with agent_status="revoked", and no tool is executed.

Your error handler can use this to trigger an incident alert, pause the agent's job queue, and notify a human operator — satisfying the EU AI Act Article 14 human oversight requirement.

Next Steps

  • Add the verify_agent_scope decorator to standalone async functions: see the Python SDK docs
  • Extend to LlamaIndex, CrewAI, or AutoGen: all four framework integrations are in kakunin.integrations
  • Set up the Supabase RLS integration to enforce database-level scope at query time: see the Supabase integration guide
  • Review the full EU AI Act compliance roadmap for AI agent operators: Navigating EU AI Act Compliance
All articles →
Read more from the blog
Documentation →
API reference and guides