Blog · Integration guide
Add human approval to a LangChain agent
When a LangChain tool can do something irreversible — deploy, refund, delete, rotate a
secret — you want a person to sign off first. Here is how to wrap such a tool so it pauses for a
payload-bound passkey approval and runs only on an explicit approved decision.
The idea
LangChain already supports a human approval pattern (interrupting before a tool call). The gap most teams hit is that the approval is just a yes/no prompt — it isn’t tied to the exact arguments the tool will run with, and there’s no durable evidence afterward. Cosignet fills that gap: the approver signs the exact tool input with a device passkey, and you get a verifiable decision back.
Cosignet is an approval and evidence layer — it doesn’t execute your tool or replace LangChain’s control flow. You call it from inside the tool, just before the side effect, and branch on the result. Because it long-polls over your outbound connection, it works from a notebook, a server, or a container with no inbound port. Background on the model: human-in-the-loop for AI agents.
1. Install
Use the Python client pattern over the REST API (the same flow the TypeScript SDK uses). You’ll need a Cosignet API key from your dashboard.
pip install langchain requests
2. A reusable approval gate
This helper creates an approval request bound to a payload, then long-polls until the human decides.
It returns only on approved; everything else raises, so the tool fails closed.
import os, time, requests
COSIGNET = "https://cosignet.com"
KEY = os.environ["COSIGNET_API_KEY"]
def require_approval(username: str, action: str, payload: dict, timeout_s: int = 300):
# 1) create the payload-bound approval request
r = requests.post(
f"{COSIGNET}/api/confirmations",
headers={"X-Api-Key": KEY, "content-type": "application/json"},
json={"username": username, "action": action, "payload": payload},
timeout=30,
)
r.raise_for_status()
cid = r.json()["id"]
# 2) long-poll for the human decision (no inbound port needed)
deadline = time.time() + timeout_s
while time.time() < deadline:
s = requests.get(
f"{COSIGNET}/api/confirmations/{cid}?wait=25",
headers={"X-Api-Key": KEY}, timeout=40,
).json()
if s["status"] != "pending":
break
if s["status"] != "approved":
raise PermissionError(f"action not approved: {s['status']}")
return s # contains the signed, payload-bound assertion
3. Wrap the high-risk tool
Call the gate at the top of the tool, with the exact arguments as the payload. If the human approves, the signature covers those arguments; if the agent had tried to run something different, the approval wouldn’t match.
from langchain_core.tools import tool
@tool
def deploy_service(service: str, environment: str, commit: str) -> str:
"""Deploy a service. Requires human approval for production."""
if environment == "production":
require_approval(
username="alex",
action=f"Deploy {service} to {environment}",
payload={"service": service, "environment": environment, "commit": commit},
)
# ... only reached if approved (or non-prod) ...
return run_deploy(service, environment, commit)
That’s the whole pattern: the tool can’t reach run_deploy for a production deploy unless a
human signed off on those exact arguments. Bind the payload to whatever makes the action unique — the
recipient and amount for a transfer, the table and filter for a deletion, the key id for a rotation.
4. Verify the decision (optional, recommended)
For the strongest guarantee, compare the approved payload to the operation you are about to run and treat any mismatch as a hard stop. Each approval is also appended to a public transparency log you can independently verify — see the verify page and the security model.
Notes
- Fails closed. Rejection, expiry, or timeout never proceeds.
- No inbound port. The long-poll runs over your outbound connection, so it works behind NAT and firewalls.
- Framework-agnostic. The same gate works in OpenAI Agents, CrewAI, a plain script, or a CI job. Agents can also call Cosignet directly over its MCP server.