Now build with it
You have an agent with an identity. Here's what you actually do.
One rule: ask before you act, sign what you did. Two calls, wherever the risky thing happens
in your code. That's the whole integration.
the pattern
your agent AgentPass your service
| | |
| 1. can i refund? | |
|------------------------>| |
| yes: L1, you declared | |
|<------------------------| |
| | |
| 2. sign the action | |
| (local key, nonce, ts) | |
| | |
| 3. call it, attach sig | |
|-------------------------------------------------->|
| | 4. verify sig + gate |
| |<----------------------|
| | valid, allowed |
| |----------------------->|
| 200 OK, and you can prove who did it |
|<--------------------------------------------------|
1 · Gate the thing before it happens
Wrap whatever you'd be nervous about. Note the except — if AgentPass is unreachable
you deny. A gate that fails open isn't a gate.
python · tools.py
import requests
AP = "https://secureagents.agentpass.co.uk"
AID = "ap_c21044400b976d6a96866af94ba26e06"
def allowed(action):
try:
r = requests.get(f"{AP}/trust/{AID}", params={"action": action}, timeout=5)
return r.json().get("allowed", False)
except Exception:
return False # can't reach the gate? then no.
def refund_customer(order_id, amount):
if not allowed("refund"):
raise PermissionError("this agent is not permitted to refund")
return stripe.Refund.create(payment_intent=order_id, amount=amount)
2 · Sign it, and make the far end check
The gate stops your own agent. The signature is what lets someone else trust it — another
service, another team, an auditor six months later.
node · agent side
// sign with the key sitting in ~/.agentpass/<agent>/private.key
const payload = JSON.stringify({
tool: "refund", args: { order: "A-1041", amount: 4200 },
agent: AID, timestamp: new Date().toISOString(),
nonce: crypto.randomBytes(16).toString("hex"),
});
const sig = crypto.createSign("SHA256").update(payload).sign(key, "base64");
await fetch("https://billing.internal/refund", {
method: "POST",
headers: { "X-Agent-Payload": payload, "X-Agent-Signature": sig },
body: payload,
});
node · the service receiving it
app.post("/refund", async (req, res) => {
const v = await fetch(`${AP}/verify`, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({
agentId: req.body.agent,
payload: req.headers["x-agent-payload"],
signature: req.headers["x-agent-signature"],
}),
}).then(r => r.json());
if (!v.valid) return res.status(403).json({ error: v.reason });
// v.reason tells you WHY: bad signature, replayed nonce,
// stale timestamp, revoked agent, or valid-but-not-allowed.
return res.json(await doRefund(req.body));
});
one signature covers all five failure modes:
forged · replayed · stale · revoked · correctly signed but not permitted.
you don't have to think about them individually. v.valid is the answer.
3 · Where it goes in your framework
It's an HTTP call, so it drops in anywhere. The useful place is the narrowest one — wrap the tool,
not the agent.
where to put the two lines
LangChain / LangGraph inside the @tool function, first line
CrewAI in the tool's _run(), one agent id per crew member
Bedrock AgentCore in the action group Lambda, before the side effect
MCP servers in the tools/call handler, before you dispatch
plain HTTP middleware. one function, every route.
rule of thumb: put the gate as close to the damage as you can get it.
Start here
Pick the one tool in your codebase you'd least like an agent to call unsupervised. Put
allowed() on it. That's a ten-minute change and it's the whole idea — everything
else is the same two lines, repeated.