CCOSIGNET

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