AttestaToolWrapper wraps a list of LangChain tools so that every invocation passes through Attesta approval. The original tool objects are mutated in-place (their func and coroutine attributes are replaced).
When a tool call is denied, the wrapper returns a string message instead of raising an exception. This allows the LLM agent to understand the denial and suggest alternatives:
"Action denied by Attesta: delete_user (risk: critical)"
Both sync (func) and async (coroutine) paths are wrapped. If your tool has a coroutine attribute, the async path is also gated. The wrapper auto-detects whether it is running inside an existing event loop (e.g., Jupyter or LangServe) and handles both cases.
import { WikipediaQueryRun } from "@langchain/community/tools/wikipedia_query";import { ChatOpenAI } from "@langchain/openai";import { gatedTool } from "@kyberon/attesta/integrations";const wiki = new WikipediaQueryRun();const safeWiki = gatedTool(wiki, { agentId: "research-agent", environment: "production", riskHints: { pii: false },});// Use safeWiki anywhere you would use wiki -- the invoke() method// now runs through Attesta approval first.const result = await safeWiki.invoke("LangChain framework");
attesta_node() returns an async function suitable for use as a LangGraph node. Insert it between the agent node and the tool-execution node so that only approved tool calls are forwarded.
from langgraph.graph import StateGraph, MessagesStatefrom langchain_openai import ChatOpenAIfrom attesta import Attestafrom attesta.integrations.langchain import attesta_nodeattesta = Attesta.from_config("attesta.yaml")llm = ChatOpenAI(model="gpt-4o")# Define nodesasync def agent_node(state: MessagesState) -> MessagesState: response = await llm.ainvoke(state["messages"]) return {"messages": [response]}async def tool_node(state: MessagesState) -> MessagesState: # Execute approved tool calls ...# Build the graphbuilder = StateGraph(MessagesState)builder.add_node("agent", agent_node)builder.add_node("gate", attesta_node(attesta)) # <-- Attesta gatebuilder.add_node("tools", tool_node)builder.add_edge("agent", "gate")builder.add_edge("gate", "tools")builder.add_edge("tools", "agent")builder.set_entry_point("agent")graph = builder.compile()# When the agent requests a tool call, the gate node evaluates it.# Denied calls are silently filtered out -- they never reach tool_node.result = await graph.ainvoke({"messages": [("user", "Delete user usr_123")]})
Denied tool calls are silently removed from the message’s tool_calls list. The agent will see fewer tool calls than it requested. This is intentional — it prevents the agent from retrying denied actions in the same turn. If you want the agent to receive explicit denial messages, use AttestaToolWrapper instead.