Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,7 @@
"group": "Production",
"pages": [
"oss/python/langgraph/application-structure",
"oss/python/langgraph/pramagent-tool-policy",
"oss/python/langgraph/test",
"oss/python/langgraph/backward-compatibility",
"oss/python/langgraph/studio",
Expand Down
133 changes: 133 additions & 0 deletions src/oss/langgraph/pramagent-tool-policy.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
---
title: Pramagent tool policy
sidebarTitle: Pramagent tool policy
---

This guide shows how to add an external policy gate in front of LangGraph tool execution with [Pramagent](https://github.com/sriram7737/pramagent). Use this pattern when a model can propose sensitive tool calls, but execution should still be checked by deterministic policy before the tool runs.

LangGraph already gives you orchestration, persistence, interrupts, and human-in-the-loop control. Pramagent complements those features by evaluating the proposed tool name and arguments before any side effect happens.

## When to use this pattern

Use an external tool policy layer when tools can:

- mutate data
- spend money
- send messages outside your system
- delete or overwrite state
- access tenant-scoped or regulated resources

The important boundary is: the model may propose a tool call, but the policy layer decides whether that call is allowed to execute.

## Install

```bash
pip install langgraph langchain pramagent
```

## Define a ToolGuard policy

Pramagent's `ToolGuardLayer` validates the tool arguments with JSON Schema, checks tenant and action allow-lists, classifies the side effect, and returns a deterministic verdict.

```python
from pramagent import Pramagent, Verdict
from pramagent.layers import ToolGuardLayer, ToolPolicy
from pramagent.layers.tool_guard import SideEffect

tool_guard = ToolGuardLayer(
policies=[
ToolPolicy(
name="send_payment",
side_effect=SideEffect.PAYMENT,
action=Verdict.ESCALATE,
allowed_tenants={"finance_team"},
schema={
"type": "object",
"required": ["amount_usd", "destination"],
"properties": {
"amount_usd": {
"type": "number",
"minimum": 0.01,
"maximum": 5000,
},
"destination": {
"type": "string",
"pattern": r"acct-\d{6,}",
},
},
"additionalProperties": False,
},
)
]
)

armor = Pramagent(tool_guard=tool_guard)
```

In this policy, valid payments are escalated for human approval. Invalid payments, such as an amount above the schema maximum or a destination that does not match the account pattern, are blocked before execution.

## Guard a LangGraph tool

Wrap the side-effecting operation so it calls `validate_tool` before performing the real action.

```python
from langchain.tools import tool
from langgraph.prebuilt import ToolNode
from pramagent import Verdict


@tool
def send_payment(amount_usd: float, destination: str) -> str:
"""Send a payment to an account."""
decision = armor.validate_tool(
"send_payment",
{"amount_usd": amount_usd, "destination": destination},
tenant_id="finance_team",
session_id="invoice-agent-demo",
action_label="payment",
)

if decision.verdict == Verdict.BLOCK:
raise PermissionError(f"Payment blocked: {decision.reason}")

if decision.verdict == Verdict.ESCALATE:
raise PermissionError(
f"Payment requires human approval before execution: {decision.reason}"
)

# Execute the real side effect only after the policy gate allows it.
return f"sent ${amount_usd:.2f} to {destination}"


tools = [send_payment]
tool_node = ToolNode(tools)
```

Add the guarded tool node to your graph the same way you would add any other `ToolNode`:

```python
from langgraph.graph import END, START, MessagesState, StateGraph

builder = StateGraph(MessagesState)
builder.add_node("tools", tool_node)
builder.add_edge(START, "tools")
builder.add_edge("tools", END)

graph = builder.compile()
```

## Combine with interrupts

If `validate_tool` returns `Verdict.ESCALATE`, route the request into a [LangGraph interrupt](/oss/python/langgraph/interrupts) instead of executing the tool immediately. Store the proposed tool call in graph state, pause for approval, and then re-run the side effect after the approval response is recorded.

For production systems, also follow the [idempotency guidance](/oss/python/langgraph/functional-api#idempotency): any external action that can run after a resume should use an idempotency key or a read-before-write check.

## What this adds

This pattern gives the graph a separate control boundary:

- LangGraph controls orchestration, state, persistence, and resumability.
- Pramagent controls whether a proposed tool call is allowed, blocked, or escalated before execution.
- The tool body remains the only place where the side effect actually happens.

That separation is useful when tool approvals need to be reviewed as policy, tested independently, and audited after the run.
Loading