Integration path SDK - wrapping agent tools with ChoxGuard Using the proxy instead? →

LangGraph + LangChain + Chox: Shadow Verdicts in 10 Minutes

Add action-level governance to your LangGraph or LangChain agent. See what your agent would have been blocked from doing — without changing a single behavior.

PyPI chox-ai-sdk

Skip the manual steps — let Claude do it

Run one command in your project directory. Claude reads your existing agent code, finds the tools, and adds Chox integration automatically.

Run this in your project's root directory (requires Claude Code). Still need an account? Complete steps 1–3 below first.

claude "Integrate the Chox AI governance SDK (chox-ai-sdk) into this project's LangGraph / LangChain agent tools.

SDK reference:
  pip install chox-ai-sdk python-dotenv
  from dotenv import load_dotenv; load_dotenv()
  from chox import ChoxGuard

  guard = ChoxGuard(
      base_url=os.getenv('CHOX_BASE_URL', 'https://chox.ai'),
      token=os.environ['CHOX_CALLER_TOKEN'],
      fail_open=True,
      on_evaluate=on_verdict,
  )

  # Wrap a single function (preserves signature; async functions detected automatically):
  wrapped = guard.wrap('integration.action', raw_fn)

  # Wrap multiple tools at once:
  wrapped_fns = guard.wrap_tools([
      ('stripe.charge', charge_fn),
      ('postgres.execute', query_fn),
  ])

  # Decorator shorthand — use BEFORE @tool (wrap the raw fn, not the StructuredTool):
  raw_search = lambda query: ...          # or a def
  wrapped_search = guard.wrap('db.search', raw_search)
  search_tool = tool(wrapped_search)      # @tool goes OUTSIDE

  # EvaluateResponse fields: verdict, shadow_verdict, shadow_rule, shadow_reason,
  #   action_type, risk_score, reason, request_id, evaluated_at
  def on_verdict(name, v):
      if v.shadow_verdict == 'block':
          print(f'[chox] {name} would have been blocked: {v.shadow_reason}')

Steps:
1. Find all agent tool functions — @tool decorators, StructuredTool.from_function(), or plain callables in tools=[].
2. Add import os if missing. Add ChoxGuard init at the top of each agent file using the env vars above.
3. For plain callables: replace fn with guard.wrap('dot.name', fn) everywhere passed to tools=[].
4. For @tool / StructuredTool: wrap the raw Python function FIRST, then pass it to @tool or StructuredTool.from_function(). Never wrap a StructuredTool object.
5. Use descriptive dot-notation names: 'stripe.create_charge', 'postgres.delete_rows', 'slack.post_message'.
6. Add chox-ai-sdk to requirements.txt or pyproject.toml if present.
7. Check for a .env file. If it exists, add CHOX_CALLER_TOKEN=your_caller_token_here to it (with a comment: # from Chox dashboard → AI Systems → Add AI System). If no .env exists, create one and add .env to .gitignore if not already there.
8. Do not change agent logic, LLM config, prompt templates, or tool descriptions."

What you'll build

A ReAct agent (LangGraph or LangChain — your choice) with two tools: a Stripe charge tool and a Postgres query tool. You'll wrap both with ChoxGuard so every tool call is evaluated by your Chox project before execution.

When the agent tries to charge $50,000 or run a DELETE FROM users query, you'll see shadow verdicts fire in real time — telling you what would have been blocked, without interrupting the agent.

Both framework paths share identical wrapping code. The only difference is the last two lines.

Prerequisites

Create a Chox project

Go to chox.ai/dashboardNew Project → give it a name and slug (e.g. "my-agent", slug "my-agent"). Copy the admin key shown once on creation.

Or via curl:

curl -X POST https://chox.ai/api/v1/projects \
  -H "Content-Type: application/json" \
  -d '{"name": "My LangGraph Agent", "slug": "my-agent"}'
# Save the admin_key from the response

Install the SDK

pip install chox-ai-sdk python-dotenv

Create a caller token

Dashboard → your project → AI SystemsAdd AI System → name it "langgraph-agent". Copy the token shown once.

Or via curl:

curl -X POST https://chox.ai/api/v1/callers \
  -H "Authorization: Bearer YOUR_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "langgraph-agent"}'
# Save the token from the response

Set up your environment

Create a .env file in your project root with the caller token from Step 3:

# .env
CHOX_CALLER_TOKEN=chox_token_paste_your_token_here
OPENAI_API_KEY=sk-...

If you don't already have one, add .env to your .gitignore to avoid committing credentials.

echo ".env" >> .gitignore

Wrap your tools with ChoxGuard

import os
from dotenv import load_dotenv; load_dotenv()
from chox import ChoxGuard, EvaluateResponse

def on_verdict(tool_name: str, v: EvaluateResponse) -> None:
    print(f"[chox] {tool_name} | verdict={v.verdict} shadow={v.shadow_verdict} risk={v.risk_score:.2f}")
    if v.shadow_verdict == "block":
        print(f"  ⚠ Would have blocked: {v.shadow_reason} (rule: {v.shadow_rule})")

guard = ChoxGuard(
    base_url="https://chox.ai",
    token=os.environ["CHOX_CALLER_TOKEN"],
    fail_open=True,
    on_evaluate=on_verdict,
)

# Your real tool implementations
def _charge_card(amount_cents: int, currency: str, description: str) -> dict:
    # In production: stripe.Charge.create(...)
    return {"status": "succeeded", "amount": amount_cents, "currency": currency}

def _run_query(sql: str) -> dict:
    # In production: execute against your database
    return {"rows": [], "affected": 0}

# Wrap them - Chox evaluates before each call
charge_card = guard.wrap("stripe.create_charge", _charge_card)
run_query   = guard.wrap("postgres.execute",     _run_query)

The on_evaluate callback fires after every Chox evaluation. It receives the tool name and an EvaluateResponse with fields: verdict, shadow_verdict, shadow_rule, shadow_reason, risk_score, reason, and signals.

With fail_open=True (the default), if the Chox gateway is unreachable, your tools still execute - you never block production because of a network blip.

Register with LangGraph or LangChain

Build your tool list the same way for both frameworks. Only the last two lines differ.

from langchain_core.tools import StructuredTool
from langchain_openai import ChatOpenAI

tools = [
    StructuredTool.from_function(
        func=charge_card,
        name="charge_card",
        description="Charge a customer's card. amount_cents is in cents (5000 = $50 USD).",
    ),
    StructuredTool.from_function(
        func=run_query,
        name="run_sql",
        description="Execute a SQL query against the database.",
    ),
]

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

LangGraph

from langgraph.prebuilt import create_react_agent

agent = create_react_agent(llm, tools)
# Run it in the next step with: agent.invoke({...})

LangChain

from langchain import hub
from langchain.agents import AgentExecutor, create_openai_tools_agent

prompt = hub.pull("hwchase17/openai-tools-agent")
agent  = create_openai_tools_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools)
# Run it in the next step with: agent_executor.invoke({...})

Set a financial threshold to trigger shadow blocks

By default, all calls log with shadow_verdict=allow. To see the governance layer actually fire, configure a financial threshold:

  1. Go to chox.ai/dashboard → your project → Settings
  2. Set the Financial threshold to a value below $50,000 — e.g. $1,000. Any financial operation above this amount will receive a shadow_verdict=block.
  3. Save. Shadow mode is still on — nothing gets blocked in production, only logged.

Now when the agent calls charge_card with $50,000, Chox will evaluate it as shadow_verdict=block and your on_verdict callback will fire the warning — while the actual charge still goes through.

Run the agent

LangGraph:

# Triggers shadow verdicts for both tool calls
response = agent.invoke({
    "messages": [
        {"role": "user", "content":
         "Charge the enterprise account $50,000 for the annual subscription, "
         "then clean up old test users: DELETE FROM users WHERE test=true"}
    ]
})
print(response["messages"][-1].content)

LangChain:

response = agent_executor.invoke({
    "input": "Charge the enterprise account $50,000 for the annual subscription, "
             "then clean up old test users: DELETE FROM users WHERE test=true"
})
print(response["output"])

Expected console output (from the on_verdict callback):

[chox] stripe.create_charge | verdict=allow shadow=block risk=0.92
  ⚠ Would have blocked: Financial operation exceeds threshold (rule: financial_above_10k)
[chox] postgres.execute | verdict=allow shadow=block risk=0.82
  ⚠ Would have blocked: Destructive SQL operation detected (rule: destructive_sql)

Both tool calls executed normally — your agent finished without interruption. But Chox recorded exactly what would have been blocked when you flip enforcement on.

See shadow verdicts in the dashboard

Go to chox.ai/dashboardLogs. You'll see both tool calls logged with:

This is the path from observation to enforcement: watch the dashboard for a week, see what would have been blocked, tune the rules if needed, then flip enforcement on with confidence.

Next steps